深入学习 Go - 00

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。如果结构体成员名字是以大写字母开头的,那么该成员就是导出的。

结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。例如,对于bytes.Buffer类型,结构体初始值就是一个随时可用的空缓存,sync.Mutex的零值也是有效的未锁定状态。有时候这种零值可用的特性是自然获得的,但是也有些类型需要一些额外的工作。

type Employee struct {
    ID            int
    Name, Address string
    DoB           time.Time
    Position      string
    Salary        int
    ManagerID     int
}

pp := &Point{1, 2}
// 等价
pp := new(Point)
*pp = Point{1, 2}

结构体成员的输入顺序也有重要的意义。我们也可以将Position成员合并(因为也是字符串类型),或者是交换Name和Address出现的先后顺序,那样的话就是定义了不同的结构体类型。通常,我们只是将相关的成员写到一起。

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含 *S 指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。下面使用一个二叉树来实现一个插入排序

type tree struct {
    value       int
    left, right *tree
}

// Sort sorts values in place.
func Sort(values []int) {
    var root *tree
    for _, v := range values {
        root = add(root, v)
    }
    appendValues(values[:0], root)
}

// appendValues appends the elements of t to values in order
// and returns the resulting slice.
func appendValues(values []int, t *tree) []int {
    if t != nil {
        values = appendValues(values, t.left)
        values = append(values, t.value)
        values = appendValues(values, t.right)
    }
    return values
}
func add(t *tree, value int) *tree {
    if t == nil {
        // Equivalent to return &tree{value: value}.
        t = new(tree)
        t.value = value
        return t
    }
    if value < t.value {
        t.left = add(t.left, value)
    } else {
        t.right = add(t.right, value)
    }
    return t
}

结构体字面值

有两种形式的结构体字面值语法,其一要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。要记住结构体的每个成员的类型和顺序,不过结构体成员有细微的调整就可能导致上述代码不能编译。只在定义结构体的包内部使用,或者是在较小的结构体中使用。

type Point struct{ X, Y int }
p := Point{1, 2}

其二,以成员名字和相应的值来初始化,可以包含部分或全部的成员,如果成员被忽略的话将默认用零值。因为提供了成员的名字,所以成员出现的顺序并不重要。

anim := gif.GIF{LoopCount: nframes}

两种不同形式的写法不能混合使用。而且不能初始化结构体中未导出成员(即小写开头的成员)

结构体可以作为函数的参数和返回值,如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回。如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

func Scale(p Point, factor int) Point {
    return Point{p.X * factor, p.Y * factor}
}
fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"

func Bonus(e *Employee, percent int) int {
    return e.Salary * percent / 100
}

结构体比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。

type Point struct{ X, Y int }

p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
// 等价
fmt.Println(p == q)                   // "false"


// map 比较 
type address struct {
    hostname string
    port int
}

hits := make(map[address]int)
hits[address{"golang.org", 443}]++

结构体嵌入和匿名成员

Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。

下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径。

其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。

type Point struct {
    X, Y int
}
type Circle struct {
    Point
    Radius int
}
type Wheel struct {
    Circle
    Spokes int
}

// 右边的注释中给出的显式形式访问这些叶子成员的语法依然有效
// 因此匿名成员并不是真的无法访问了。
var w Wheel
w.X = 8      // equivalent to w.Circle.Point.X = 8
w.Y = 8      // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20

结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}
fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
w.X = 42
fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}

因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所以匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员,但是在包外部,因为circle和point没有导出,不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。

w.X = 8 // equivalent to w.circle.point.X = 8

简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心,

组合

内嵌可以使我们在定义ColoredPoint时得到一种句法上的简写形式,并使其包含Point类型所具有的一切字段,然后再定义一些自己的。

可以把ColoredPoint类型当作接收器来调用Point里的方法,即使ColoredPoint里没有声明这些方法。Point类的方法也被引入了ColoredPoint。用这种方式,内嵌可以使我们定义字段特别多的复杂类型,我们可以将字段先按小类型分组,然后定义小类型的方法,之后再把它们组合起来。

import "image/color"

type Point struct{ X, Y float64 }

type ColoredPoint struct {
    Point
    Color color.RGBA
}

// 第一种
var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"

// 第二种
red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"

一个ColoredPoint并不是一个Point,但他”has a”Point,并且它有从Point类里引入的Distance和ScaleBy方法。

在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。

type ColoredPoint struct {
    *Point
    Color color.RGBA
}
p := ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point // p and q now share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"

一个struct类型也可能会有多个匿名字段。

type ColoredPoint struct {
    Point
    color.RGBA
}

然后这种类型的值便会拥有Point和RGBA类型的所有方法,以及直接定义在ColoredPoint中的方法。当编译器解析一个选择器到方法时,比如p.ScaleBy,它会首先去找直接定义在这个类型里的ScaleBy方法,然后找被ColoredPoint的内嵌字段们引入的方法,然后去找Point和RGBA的内嵌字段引入的方法,然后一直递归向下找。如果选择器有二义性的话编译器会报错,比如你在同一级里有两个同名的方法。

package main

import "fmt"

type Father struct {
    MingZi string
    Age    int
}

func (f *Father) Say() string {
    return "大家好,我叫 " + f.MingZi
}

type Mother struct {
    Name string
    Age  int
}

func (m *Mother) Say() string {
    return "Hello, my name is " + m.Name
}

type Child struct {
    Father
    Mother
}

func main() {
    c := new(Child)
    c.MingZi = "张小明"
    c.Name = "Tom Zhang"

    // 因为都有 Age ,所以出现冲突
    c.Age = 18 // error: ambiguous selector c.Age

    // 因为都有 Say 方法,所以出现冲突
    c.Say() // error: ambiguous selector c.Say

    // 正确
    fmt.Println(c.Father.Say())
    fmt.Println(c.Mother.Say())
}
package main

import "fmt"

type Father struct {
    Name string
}

func (f *Father) Say() {
    fmt.Println("大家好,我叫", f.Name)
}

type Child struct {
    Father
}

func (c *Child) Say() {
    fmt.Println("Chile")
}

func main() {
    c := new(Child)
    c.Name = "张小明"

    // 由于检索顺序不同,所以执行 child 的 say()
    c.Say()
    c.Father.Say()
}

// output:
// Chile
// 大家好,我叫 张小明

因为sync.Mutex字段也被嵌入到了这个struct里,其Lock和Unlock方法也就都被引入到了这个匿名结构中了,这让我们能够以一个简单明了的语法来对其进行加锁解锁操作。

var (
    mu sync.Mutex // guards mapping
    mapping = make(map[string]string)
)
func Lookup(key string) string {
    mu.Lock()
    v := mapping[key]
    mu.Unlock()
    return v
}

//这两个版本在功能上是一致的,但将两个包级别的变量放在了cache这个struct一组内
var cache = struct {
    sync.Mutex
    mapping map[string]string
}{
    mapping: make(map[string]string),
}

func Lookup(key string) string {
    cache.Lock()
    v := cache.mapping[key]
    cache.Unlock()
    return v
}

参考链接