27

Java程序员学习Go指南(二)

 4 years ago
source link: https://www.luozhiyun.com/archives/211
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go中的结构体

构建结构体

如下:

type AnimalCategory struct {
    kingdom string // 界。
    phylum  string // 门。
    class   string // 纲。
    order   string // 目。
    family  string // 科。
    genus   string // 属。
    species string // 种。
}

func (ac AnimalCategory) String() string {
    return fmt.Sprintf("%s%s%s%s%s%s%s",
        ac.kingdom, ac.phylum, ac.class, ac.order,
        ac.family, ac.genus, ac.species)
}

我们在Go中一般构建一个结构体由上面代码块所示。AnimalCategory结构体中有7个string类型的字段,下边有个名叫String的方法,这个方法其实就是java类中的toString方法。其实这个结构体就是java中的类,结构体中有属性,有方法。

category := AnimalCategory{species: "cat"} 
fmt.Printf("The animal category: %s\n", category)

我们在上面的代码块中初始化了一个AnimalCategory类型的值,并把它赋给了变量category,通过调用fmt.Printf方法调用了category实例内的String方法,⽽⽆需 显式地调⽤它的String⽅法。

在结构体中声明一个嵌入字段

因为在Go中是没有继承一说,所以使用了嵌入字段的方式来实现类型之间的组合,实现了方法的重用。

这里继续用到上面的结构体AnimalCategory

type Animal struct {
    scientificName string // 学名。
    AnimalCategory        // 动物基本分类。
}

字段声明AnimalCategory代表了Animal类型的⼀个嵌⼊字段。Go语⾔规范规定,如果⼀个字段 的声明中只有字段的类型名⽽没有字段的名称,那么它就是⼀个嵌⼊字段,也可以被称为匿名字段。嵌⼊字段的类型既是类型也是名称。

如果要像java中引用字段里面的属性,那么可以这么写:

func (a Animal) String() string {
    return a.AnimalCategory.String()
}

这里还是和java是一样的,但是接下来要讲的却和java有很大区别

由于我们在AnimalCategory中写了一个String的方法,如果我们没有给Animal写String的方法,那么我们直接打印会得到什么结果?

category := AnimalCategory{species: "cat"}

    animal := Animal{
        scientificName: "American Shorthair",
        AnimalCategory: category,
    }
    fmt.Printf("The animal: %s\n", animal)

在这里fmt.Printf函数相当于调用animal的String⽅法。在java中只有父类才会做到方法的覆盖,但是在Go中,嵌⼊字段的⽅法集合会被⽆条件地合并进被嵌⼊类型的⽅法集合中。

如果为Animal类型编写⼀个String⽅法,那么会将嵌⼊字段AnimalCategory的String⽅法被“屏蔽”了,从而调用Animal的String方法。

只 要名称相同,⽆论这两个⽅法的签名是否⼀致,被嵌⼊类型的⽅法都会“屏蔽”掉嵌⼊字段的同名⽅法。也就是说不管返回值类型或者方法参数如何,只要名称相同就会屏蔽掉嵌⼊字段的同名⽅法。

指针方法

上面我们的例子其实都是值方法,下面我们举一个指针方法的例子:

func main() {
    cat := New("little pig", "American Shorthair", "cat")
    cat.SetName("monster") // (&cat).SetName("monster")
    fmt.Printf("The cat: %s\n", cat)

    cat.SetNameOfCopy("little pig")
    fmt.Printf("The cat: %s\n", cat)

}
type Cat struct {
    name           string // 名字。
    scientificName string // 学名。
    category       string // 动物学基本分类。
}
//构造一个cat实例
func New(name, scientificName, category string) Cat {
    return Cat{
        name:           name,
        scientificName: scientificName,
        category:       category,
    }
}
//传指针设置cat名字
func (cat *Cat) SetName(name string) {
    cat.name = name
}
//传入值
func (cat Cat) SetNameOfCopy(name string) {
    cat.name = name
}
func (cat Cat) String() string {
    return fmt.Sprintf("%s (category: %s, name: %q)",
        cat.scientificName, cat.category, cat.name)
}

在这个例子中,我们为Cat设置了两个方法,SetName是传指针的方法,SetNameOfCopy是传值的方法。

⽅法SetName的接收者类型是 Cat。Cat左边再加个 代表的就是Cat类型的指针类型。

我们通过运行上面的例子可以得出, 值⽅法 的接收者是该⽅法所属的那个 类型值 的⼀个副本。⽽指针⽅法的接收者,是该⽅法所属的那个基本 类型值的指针值 的⼀个副本。我们在这样的⽅法内对该副本指向的值进⾏ 修改,却⼀定会体现在原值上。

接口类型

声明

type Pet interface {
    SetName(name string)
    Name() string
    Category() string
}

当数据类型中的方法实现了接口中的所有方法,那么该数据类型就是该接口的实现类型,如下:

type Pet interface {
    Name() string
    Category() string
    SetName(name string)
}

type Dog struct {
    name string // 名字。
}

func (dog *Dog) SetName(name string) {
    dog.name = name
}

func (dog Dog) Name() string {
    return dog.name
}

func (dog Dog) Category() string {
    return "dog"
}

在这里Dog类型实现了Pet接口。

接口变量赋值

接口变量赋值也涉及了值传递和指针传递的概念。如下:

// 示例1
    dog := Dog{"little pig"}
    fmt.Printf("The dog's name is %q.\n", dog.Name())
    var pet Pet = dog
    dog.SetName("monster")
    fmt.Printf("The dog's name is %q.\n", dog.Name())
    fmt.Printf("This pet is a %s, the name is %q.\n",
        pet.Category(), pet.Name())
    fmt.Println()

    // 示例2。
    dog = Dog{"little pig"}
    fmt.Printf("The dog's name is %q.\n", dog.Name())
    pet = &dog
    dog.SetName("monster")
    fmt.Printf("The dog's name is %q.\n", dog.Name())
    fmt.Printf("This pet is a %s, the name is %q.\n",
        pet.Category(), pet.Name())

返回

The dog's name is "little pig".
The dog's name is "monster".
This pet is a dog, the name is "little pig".

The dog's name is "little pig".
The dog's name is "monster".
This pet is a dog, the name is "monster".

在示例1中,赋给pet变量的实际上是dog的一个副本,所以当dog设置了name的时候pet的name并没发生改变。

在实例2中,赋给pet变量的是一个指针的副本,所以pet和dog一样发生了编发。

接口之间的组合

可以通过接口间的嵌入实现接口的组合。接⼝类型间的嵌⼊不会涉及⽅法间的“屏蔽”。只要组合的接⼝之间有同名的⽅法就会产⽣冲突,从⽽⽆ 法通过编译,即使同名⽅法的签名彼此不同也会是如此。

type Animal interface {
    // ScientificName 用于获取动物的学名。
    ScientificName() string
    // Category 用于获取动物的基本分类。
    Category() string
}

type Named interface {
    // Name 用于获取名字。
    Name() string
}

type Pet interface {
    Animal
    Named
}

指针

哪些值是不可寻址的

  1. 不可变的变量

如果一个变量是不可变的,那么基于它的索引或切⽚的结果值都是不可寻址的,因为即使拿到了这种值的内存地址也改变不了什么。

如:

const num = 123
    //_ = &num // 常量不可寻址。
    //_ = &(123) // 基本类型值的字面量不可寻址。

    var str = "abc"
    _ = str
    //_ = &(str[0]) // 对字符串变量的索引结果值不可寻址。
    //_ = &(str[0:2]) // 对字符串变量的切片结果值不可寻址。
    str2 := str[0]
    _ = &str2 // 但这样的寻址就是合法的。
  1. 临时结果

在我们把临时结果值赋给任何变量或常量之前,即使能拿到它的内存地址也是没有任何意义的。所以也是不可寻址的。

我们可以把各种对值字⾯量施加的表达式的求值结果都看做是 临时结果。

如:

  • ⽤于获得某个元素的索引表达式。
  • ⽤于获得某个切⽚(⽚段)的切⽚表达式。
  • ⽤于访问某个字段的选择表达式。
  • ⽤于调⽤某个函数或⽅法的调⽤表达式。
  • ⽤于转换值的类型的类型转换表达式。
  • ⽤于判断值的类型的类型断⾔表达式。
  • 向通道发送元素值或从通道那⾥接收元素值的接收表达式。

⼀个需要特别注意的例外是,对切⽚字⾯量的索引结果值是可寻址的。因为不论怎样,每个切⽚值都会持有⼀个底层数组,⽽ 这个底层数组中的每个元素值都是有⼀个确切的内存地址的。

//_ = &(123 + 456) // 算术操作的结果值不可寻址。
//_ = &([3]int{1, 2, 3}[0]) // 对数组字面量的索引结果值不可寻址。
//_ = &([3]int{1, 2, 3}[0:2]) // 对数组字面量的切片结果值不可寻址。
_ = &([]int{1, 2, 3}[0]) // 对切片字面量的索引结果值却是可寻址的。
//_ = &([]int{1, 2, 3}[0:2]) // 对切片字面量的切片结果值不可寻址。
//_ = &(map[int]string{1: "a"}[0]) // 对字典字面量的索引结果值不可寻址。
  1. 不安全

    函数在Go语⾔中是⼀等公⺠,所以我们可以把代表函数或⽅法的字⾯量或标识符赋给某个变量、传给某个函数或者从某个函数传出。

但是,这样的函数和⽅法都是不可寻址的。⼀个原因是函数就是代码,是不可变的。另⼀个原因是,拿到指向⼀段代码的指针是不安全的。

此外,对函数或⽅法的调⽤结果值也是不可寻址的,这是因为它们都属 于临时结果。

如:

//_ = &(func(x, y int) int {
    //  return x + y
    //}) // 字面量代表的函数不可寻址。
    //_ = &(fmt.Sprintf) // 标识符代表的函数不可寻址。
    //_ = &(fmt.Sprintln("abc")) // 对函数的调用结果值不可寻址。

goroutine协程

在Go语言中,协程是由go函数进行触发的,当程序执⾏到⼀条go语句的时候,Go语⾔ 的运⾏时系统,会先试图从某个存放空闲的G的队列中获取⼀个G(也就是goroutine),它只有在找不到空闲G的情况下才会 去创建⼀个新的G。

故已存在的goroutine总是会被优先复⽤。

在拿到了⼀个空闲的G之后,Go语⾔运⾏时系统会⽤这个G去包装当前的那个go函数(或者说该函数中的那些代码),然后再 把这个G追加到某个存放可运⾏的G的队列中。

在Go语⾔并不会去保证这些goroutine会以怎样的顺序运⾏。所以哪个goroutine先执⾏完、哪个goroutine后执⾏完往往是不可预知的,除⾮我们使⽤了某种Go语⾔提供的⽅式进⾏了⼈为 ⼲预。

所以,怎样让我们启⽤的多个goroutine按照既定的顺序运⾏?

多个goroutine按照既定的顺序运⾏

下面我们先看个例子:

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
}

在下面的代码中,由于Go语言并不会按顺序去执行调度,所以没法知道fmt.Println(i)会在什么时候被打印,也不知道fmt.Println(i)打印的时候i是多少,也有可能main方法执行完了,但是没有一条输出。

所以我们需要进行如下改造:

func main() {
    var count uint32
    trigger := func(i uint32, fn func()) {
        for {
            if n := atomic.LoadUint32(&count); n == i {
                fn()
                atomic.AddUint32(&count, 1)
                break
            }
            time.Sleep(time.Nanosecond)
        }
    }
    for i := uint32(0); i < 10; i++ {
        go func(i uint32) {
            fn := func() {
                fmt.Println(i)
            }
            trigger(i, fn)
        }(i)
    }
    trigger(10, func() {})
}

我们在for循环中声明了一个fn函数,fn函数里面只是简单的执行打印i的值,然后传入到trigger中。

trigger函数会不断地获取⼀个名叫count的变量的值,并判断该值是否与参数i的值相同。如果相同,那么就⽴即调⽤fn代 表的函数,然后把count变量的值加1,最后显式地退出当前的循环。否则,我们就先让当前的goroutine“睡眠”⼀个纳秒再进 ⼊下⼀个迭代。

因为会有多个线程操作trigger函数,所以使用的count变量是通过原子操作来进行获取值和加一操作。

所以过函数实际执行顺序会根据count的值依次执行,这里实现了一种自旋,未满足条件的时候会不断地进行检查。

最后防止主协程在其他协程没有运行完的时候就关闭,加上一个 trigger(10, func() {}) 代码。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK