Go语言核心之美 3.4-Struct结构体

struct(结构体)也是一种聚合的数据类型,struct可以包含多个任意类型的值,这些值被称为struct的字段。用来演示struct的一个经典案例就是雇员信息,每条雇员信息包含:员工编号,姓名,住址,出生日期,工作岗位,薪资,直属领导等。每个雇员的所有信息都可以存在一个struct中,该struct可以作为变量,或者作为函数的参数、返回值,或者被存到数组、切片中,等等。

下面声明了一个Employee类型的结构体,还声明了一个Employee类型的变量dilbert:

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

var dilbert Employee

dilbert的struct字段可以通过点操作符来访问,例如dilbert.Name和dilbert.Dob。同时dilbert的字段是变量,因此可以直接对成员赋值:

dilbert.Salary -= 5000 // demoted, for writing too few lines of code

也可以对成员进行取址,然后通过指针访问:

position := &dilbert.Position
*position = "Senior " + *position // promoted, for outsourcing to Elbonia

点操作符还可以应用在struct指针上:

var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"

上面的第二条语句相当于:

(*employeeOfTheMonth).Position += " (proactive team player)"

EmployeeByID函数根据给定的员工编号返回一个指针指向包含员工信息的struct,可以使用点操作符来访问里面的字段:

func EmployeeByID(id int) *Employee { /* ... */ }

fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Pointy-haired boss"

id := dilbert.ID
EmployeeByID(id).Salary = 0 // fired for... no real reason

最后那条语句要注意,它调用EmployeeByID生成了一个*Employee指针,然后直接更新该结构体中的一个字段。如果将EmployeeByID的返回值从*Employee换成Employee类型,那么编译将报错,因为编译器无法对返回的Employee进行寻址(不通过变量直接使用一个值,一般都无法进行寻址)。

struct声明时,通常是一行写一个字段,字段名在类型之前,不过如果两个字段类型相同,那么可以合并到一行,例如Name和Address:

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

对于struct类型来说,字段的先后顺序是非常关键的。如果两个struct类型包含了完全相同的字段,但是排列顺序不同或者进行了部分合并,那么这两个struct就是不同的类型!

如果struct字段是大写字母开头,那么该字段就是导出的(包外可见),这也符合Go语言的可见性规则。因此一个struct可以同时包含导出和未导出的变量。

我们可以使用匿名struct,特别是在一个struct只是临时使用时,例如:

 var user = struct{
		id	int
		name string
	} {
		id : 1,
		name: "corego",
	}

但是对于常用的struct类型来说,这样写就会很麻烦,因此应该使用具名struct,例如之前的Employee。

一个结构体S不能再包含S类型的字段,因为聚合类型的值不能包含它自身(数组也是一样)。但是S可以包含*S类型的字段,利用这个特性,我们可以创建链表、树这样的递归数据结构。下面的代码使用了二叉树来实现插入排序:

gopl.io/ch4/treesort

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
}

一个struct的零值意味着它的每个成员都是零值,因此struct初始化为零值是最理想的默认情况。例如,对于bytes.Buffer,它的零值就是一个空缓存,sync.Mutex的零值是未使用的互斥锁。零值在大多数情况下是可以直接使用的,但是有些时候需要一些额外的工作。

没有任何字段的struct就是空结构体,写作struct{}, 它不包含任何信息,大小也为0,在有些场景下这种空struct是有价值的。有些Go程序员在用map实现set时,用空struct来代替bool值,因为他们觉得这样可以强调key的重要性。虽然这样可以节约一点空间,但是带来了更高的代码复杂度和更低的可读性,因此我们应该避免这样使用:

seen := make(map[string]struct{}) // set of strings
// ...
if _, ok := seen[s]; !ok {
    seen[s] = struct{}{}
    // ...first time seeing s...
}

4.4.1. struct字面值

struct的值可以用struct字面值来表示:

type Point struct{ X, Y int }

p := Point{1, 2}

struct字面值有两种形式,上面的代码是第一种写法:按照声明时字段的顺序依次赋值。这样写法要求写代码、读代码时要记住struct的每个字段和顺序,如果后面需要对struct进行改动,那所有的初始化代码都会出错。因此这种写法一般只在临时的或者较小的struct中使用,而且这些struct的字段是很有规律的,例如image.Point{x,y},color.RGBA{red,green,blue,alpha}。

实际实践中,第二种写法会更通用,使用字段名和对应的值来初始化,既可以初始化部分字段也可以初始化全部字段:

anim := gif.GIF{LoopCount: nframes}

这种写法中,如果某个字段被省略,那么该字段会初始化为相应的零值;由于提供了字段名,字段初始化的顺序也不重要。

注意,这两种写法是不能混用的,在使用第一种写法时,试图通过忽略字段名的形式来隐式初始化未导出的字段是不可行的。

package p
type T struct{ a, b int } // a and b are not exported

package q
import "p"
var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
var _ = p.T{1, 2}       // compile error: can't reference a, b

上面报错的代码中,虽然没有显式的使用未导出的字段,但是这样隐式使用的行为也是不允许的。

struct可以作函数的参数和返回值,例如Scale函数将Point进行按比例缩放后再返回:

func Scale(p Point, factor int) Point {
    return Point{p.X * factor, p.Y * factor}
}

fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"

出于效率方面的考虑,当使用较大的struct时,常常需要使用指针(这里要注意,指针参与gc,值不参与gc且函数调用完就会销毁,因此,对于小的struct,可能使用值会更好):

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

如果函数需要修改传入的struct参数,那么就必须使用指针。在Go语言中,所有的函数传参都是通过值拷贝实现的,因此传入的参数不再是之前的变量。

func AwardAnnualRaise(e *Employee) {
    e.Salary = e.Salary * 105 / 100
}

通常来说struct都是作为指针来使用,因此可以使用短声明的方法来初始化一个struct并获取它的地址:

pp := &Point{1, 2}

和下面的语句是等价的

pp := new(Point)
*pp = Point{1, 2}

不过&Point{1, 2}可以直接在表达式中使用,例如一个函数调用。

4.4.2. struct的比较

如果struct的所有字段都可以比较,那么该struct也可以通过==或!=进行比较。相等比较时,两个struct的每个字段都会进行比较,因此下面两个表达式是等价的:

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"

可以比较的struct类型就像其它可比较类型一样,可以作为map的key类型:

type address struct {
    hostname string
    port     int
}

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

4.4.3. 嵌入和匿名字段

下面我们将学习如何使用Go语言的嵌入机制,一个具名struct A是另一个具名structB的匿名字段,那么就说A是B的嵌入字段。这样,就可以通过点操作符x.f来访问匿名字段链中的x.d.e.f。

考虑一个二维绘图程序,提供一个各种图形的库,例如矩形、椭圆形、星形等几何形状:

type Circle struct {
    X, Y, Radius int
}

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

Circle代表的是圆形,包含了圆心X、Y坐标及Radius半径。Wheel轮形除了包含Cirecle的所有字段之外还增加了Spokers:

var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20

随着库中几何图形的增多,我们一定会注意到这些图形间的相似和重复之处,所以可以将相同的属性提取出来:

type Point struct {
    X, Y int
}

type Circle struct {
    Center Point
    Radius int
}

type Wheel struct {
    Circle Circle
    Spokes int
}

这样改动后库的设计更清晰了,但是会导致更冗余的访问方式:

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20

Go语言可以让我们在声明struct时无需指定字段名,只要提供字段类型即可,这种就是匿名字段。匿名字段的数据类型必须是一个具名类型或者指向具名类型的指针。下面代码中,Circle和Wheel各有一个匿名字段,这里Point类型被嵌入到Circle中,同时 Circle被嵌入到Wheel中。

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

上面的注释中,给出了显式访问这些叶子的语法,因此匿名字段依然是可以通过类型名来访问的。这里匿名字段Circle和Point都有自己的名字,但是这些名字在点操作符中是可选的。综上所述,在访问叶子字段时,可以忽略任何的匿名字段:w.X,忽略了Cirecle和Point两个匿名字段。

struct字面值初始化时,不能用短声明形式来初始化struct中的匿名字段,因此下面的语句是无法编译通过的:

w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

可以采用下面两种声明语法,它们是等价的:

gopl.io/ch4/embed

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}

注意上面Printf函数中的%#v参数,#代表用Go的语法来打印。对于struct类型来说,打印中将包含每个字段的信息(强烈建议读者亲自试验这个#)。

因为匿名字段也有隐式的名字,因此不能同时包含两个相同的匿名字段,会导致名字冲突。由于字段名是隐式的决定的,所以匿名字段的可见性也是隐式决定的。在上面例子中,Point和Circle两个匿名字段都是导出的,然而,即使它们不导出(比如point和circle),我们依然可以访问这两个匿名字段中的字段:

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

但是在circle所在的包之外,上面这条语句评论中的长赋值形式是不允许的,因为circle是未导出的!

可以看出点操作符只是一种访问匿名字段的语法糖,在后面我们会看到匿名字段并不仅仅是struct类型,任何具名类型都可以作为匿名字段。但是为什么要嵌入一个非struct(没有任何字段)的匿名类型呢?

答案就是method(方法)。点操作符不仅可以选择匿名字段的子字段,也可以访问它们的方法。实际上最外层的struct不仅仅获得了匿名字段的所有子字段,还获得了它们的全部导出的方法。我们可以利用这个机制实现Go语言中面向对象编程的核心概念:组合,将在第五章中专门讨论。

文章所有权:Golang隐修会 联系人:孙飞,CTO@188.com!

    原文作者:Sunface撩技术
    原文地址: https://blog.csdn.net/erlib/article/details/50970761
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞