结构体

Go提供的结构体就是把使用各种数据类型定义的不同变量组合起来的高级数据类型。

  1. type Rectangle struct {
  2. width float64
  3. length float64
  4. }

通过type定义一个新的数据类型,然后是新的数据类型名称rectangle,最后是struct关键字,表示这个高级数据类型是结构体类型。

计算一下矩形面积。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Rectangle struct {
  6. width float64
  7. length float64
  8. }
  9. func main() {
  10. var r Rectangle
  11. r.width = 100
  12. r.length = 200
  13. fmt.Println(r.width * r.length)
  14. }

其实构体类型和基础数据类型使用方式差不多,唯一的区别就是结构体类型可以通过.来访问内部的成员。包括给内部成员赋值和读取内部成员值。

在这里我们是用var关键字先定义了一个rectangle变量,然后对它的成员赋值。我们也可以使用初始化的方式来给rectangle变量的内部成员赋值。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Rect struct {
  6. width float64
  7. length float64
  8. }
  9. func main() {
  10. var r = Rectangle{width: 100, length: 200}
  11. fmt.Println(r.width * r.length)
  12. }

如果你知道结构体成员定义的顺序,也可以不使用key:value的方式赋值,直接按照结构体成员定义的顺序给它们赋值。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Rectangle struct {
  6. width float64
  7. length float64
  8. }
  9. func main() {
  10. var r = Rectangle{100, 200}
  11. fmt.Println("Width:", r.width, "* Length:",r.length, "= Area:", r.width*r.length)
  12. }

输出结果为:

  1. Width: 100 * Length: 200 = Area: 20000

结构体参数传递方式,Go函数的参数传递方式是值传递,这句话对结构体也是适用的。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Rectangle struct {
  6. width float64
  7. length float64
  8. }
  9. func double_area(r Rectangle) float64 {
  10. r.width *= 2
  11. r.length *= 2
  12. return r.width * r.length
  13. }
  14. func main() {
  15. var r = Rectangle{100, 200}
  16. fmt.Println(double_area(r))
  17. fmt.Println("Width:", r.width, "Length:", r.length)
  18. }

输出为:

  1. 80000
  2. Width: 100 Length: 200

虽然在double_area函数里面我们将结构体的宽度和长度都加倍,但仍然没有影响main函数里面的rect变量的宽度和长度。这是为何呢?

我们看到,虽然main函数中的rect变量可以直接调用函数area()来获取矩形面积,但是area()函数确实没有定义在Rect结构体内部,这点和C语言的有很大不同。Go使用组合函数的方式来为结构体定义结构体方法。我们仔细看一下上面的area()函数定义。

首先是关键字func表示这是一个函数,第二个参数是结构体类型和实例变量,第三个是函数名称,第四个是函数返回值。这里我们可以看出area()函数和普通函数定义的区别就在于area()函数多了一个结构体类型限定。这样一来Go就知道了这是一个为结构体定义的方法。

要想改变函数输出的长方形的值,我们使用指针可以做到。指针的主要作用就是在函数内部改变传递进来变量的值。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Rectangle struct {
  6. width float64
  7. length float64
  8. }
  9. func (r *Rectangle) area() float64 {
  10. return r.width * r.length
  11. }
  12. func main() {
  13. var r = new(Rectangle)
  14. r.width = 100
  15. r.length = 200
  16. fmt.Println("Width:", r.width, "Length:", r.length,"Area:", r.area())
  17. }

使用了new函数来创建一个结构体指针Rectangle,也就是说r的类型是Rectangle,结构体遇到指针的时候,你不需要使用去访问结构体的成员,直接使用.引用就可以了。所以上面的例子中我们直接使用r.width=100 和r.length=200来设置结构体成员值。因为这个时候r是结构体指针,所以我们定义area()函数的时候结构体限定类型为*Rectangle。

其实在计算面积的这个例子中,我们不需要改变矩形的宽或者长度,所以定义area函数的时候结构体限定类型仍然为Rectangle也是可以的。如下:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Rectangle struct {
  6. width float64
  7. length float64
  8. }
  9. func (r Rectangle) area() float64 {
  10. return r.width * r.length
  11. }
  12. func main() {
  13. var r = new(Rectangle)
  14. r.width = 100
  15. r.length = 200
  16. fmt.Println("Width:", r.width, "Length:", r.length,"Area:", r.area())
  17. }

通常有人就会有疑问觉得不知道啥时候使用指针,啥时候不使用指针,其实呢使不使用指针,取决于你是否试图在函数内部改变传递进来的参数的值。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Rectangle struct {
  6. width float64
  7. length float64
  8. }
  9. func (r *Rectangle) double_area() float64 {
  10. r.width *= 2
  11. r.length *= 2
  12. return r.width * r.length
  13. }
  14. func main() {
  15. var r = new(Rectangle)
  16. r.width = 100
  17. r.length = 200
  18. fmt.Println(*r)
  19. fmt.Println("Double Width:", r.width, "Double Length:", r.length,"Double Area:", r.double_area())
  20. fmt.Println(*r)
  21. }
  • 结构体特性

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员. 直白的讲就是首字母大写的结构体字段可以被导出,也就是说,在其他包中可以进行读写。结构体字段名以小写字母开头是当前包的私有的,函数定义也是类似的。

如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。有些Go开发者用map模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所有我们通常避免避免这样的用法。

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

  1. type tree struct {
  2. value int
  3. left, right *tree
  4. }
  • 结构体嵌入和匿名成员
  1. type Point struct {
  2. X, Y int
  3. }
  4. type Circle struct {
  5. Center Point
  6. Radius int
  7. }
  8. type Wheel struct {
  9. Circle Circle
  10. Spokes int
  11. }

访问每个成员:

  1. var w Wheel
  2. w.Circle.Center.X = 8
  3. w.Circle.Center.Y = 8
  4. w.Circle.Radius = 5
  5. w.Spokes = 20

Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。

  1. type Circle struct {
  2. Point // 匿名字段,struct
  3. Radius int
  4. }
  5. type Wheel struct {
  6. Circle // 匿名字段,struct
  7. Spokes int
  8. }

得意于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

  1. var w Wheel
  2. w.X = 8 // equivalent to w.Circle.Point.X = 8
  3. w.Y = 8 // equivalent to w.Circle.Point.Y = 8
  4. w.Radius = 5 // equivalent to w.Circle.Radius = 5
  5. w.Spokes = 20

在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。

结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:

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

结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的. 通过这个我们看出来struct不仅仅能够将struct作为匿名字段,自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作。

  • 结构体tag

在Golang中结构体和数据库表的映射关系的建立是通过struct Tag来实现的。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect" // 这里引入reflect模块
  5. )
  6. type User struct {
  7. Name string `json:"name"` //这引号里面的就是tag
  8. Passwd int `json:"passwd"`
  9. }
  10. func main() {
  11. user := &User{"keke", 123456}
  12. s := reflect.TypeOf(user).Elem() //通过反射获取type定义
  13. for i := 0; i < s.NumField(); i++ {
  14. fmt.Println(s.Field(i).Tag.Get("json")) //将tag输出出来
  15. }
  16. }

运行 :

  1. name
  2. passwd

结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:”value”键值对序列;因为值中含义双引号字符,因此成员Tag一般用原生字符串面值的形式书写。

这里有必要解释下指针这块,在Golang中很多时候都是需要用指针结合结构体开发的。 通常指针是存储一个变量的内存地址的变量。

在Golang中,指针不参与计算但是可以用来获取地址的,例如变量a的内存地址为&a,这里的&就是获取a的地址。如果一个指针,它的值是在别的地方的地址,而且我们想要获取这个地址的值,可以使用符号。符号是为取值符。例如上面的&a是一个地址,那么这个地址里存储的值为*&a。

注意:这里的 & 和 的区别,& 运算符,用来获取指针地址,而 运算符是用来获取地址里存储的值。此外指针的值和指针的地址是不同的概念,指针的值: 指的是一个地址,是别的内存地址。指针的地址: 指的是存储指针内存块的地址。

通常 & 运算符是对变量取地址,如:变量a的地址是&a, 运算符对指针取值,如:`&a,就是a变量所在地址的值,也就是a的值.此外 &和 * 以互相抵消,同时注意, & 可以抵消掉,但 & 是不可以抵消的, a和 &a是一样的,都是a的值,因为 & 互相抵消掉了,同理a和 &&&&a 是一样的 (因为4个 *& `互相抵消掉了)。

  1. var a = 2
  2. var b *int = &a
  3. 所以a和*&a和*b是一样的,都是a的值,值为2 (把b当做&a看)

应用示例:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main(){
  6. b := 200
  7. a := &b
  8. fmt.Println("the address of b:",a)
  9. fmt.Println("the value of b:",*a)
  10. var p *int //p的类型是[int型的指针]
  11. p = &b //p的值为 [b的地址]
  12. fmt.Printf("b=%d,p=%d,*p=%d \n",b,p,*p)
  13. *p = 5 // *p的值为[[b的地址]的指针] (其实就是b),这行代码也就等价于b= 5
  14. fmt.Printf("b=%d,p=%d,*p=%d\n",b,p,*p)
  15. }

运行:

  1. the address of b: 0xc4200180b8
  2. the value of b: 200
  3. b=200,p=842350559416,*p=200
  4. b=5,p=842350559416,*p=5

通常我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。

传指针比较轻量级 (*bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。

Golang中string,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。

注意:若函数需改变slice的长度,则仍需要取地址传递指针。

如果要访问指针 p 指向的结构体中某个元素 x,不需要显式地使用 * 运算,可以直接 p.x;