1. 定义

结构体是将另个或者多个任意类型的命名变量组合在一起的聚合数据类型。

2. 成员变量

  • 访问控制机制
    如果一个结构体的成员变量名称是首字母大些的,那么这个变量是可导出的(即在其他包可以访问到)。
    一个结构体可以同时包含可导出和不可导出的成员变量
type A struct {
    Hour int    //可导出
    minute int  //不可导出
}
  • 限制
    命名结构体类型s不可以定义一个拥有相同结构体类型s的成员变量,也就是一个聚合类型不可以包含它自己。但是s中可以定义一个s的指针类型,即*s。如下:
type B struct {
    value int
    //Next B   //错误
    Next *B    //正确
}

3. 结构体比较

  1. 如果结构体的所有成员变量都可以比较,那么这个结构体是可以比较的。两个结构体的比较可以使用==或者!=。
type C struct {
    A int
    B string
}
c1 := C{A:1, B:"abc"}
c2 := C{A:1, B:"abc"}
c3 := C{A:2, B:"abc"}
fmt.Println(c1.A == c2.A && c1.B == c2.B) //true
fmt.Println(c1 == c2)                        //true 与上等价
fmt.Println(c1.A == c3.A && c1.B == c3.B) //false
fmt.Println(c1 == c3)                        //false 与上等价

2.和其他可比较的类型一样,可比较的结构体类型都可以作为map的键类型。

type C struct {
    A int
    B string
}
mp := make(map[C]int)
key := C{A:1, B:"abc"}
mp[key] = 9
fmt.Println(mp[C{A:1, B:"abc"}])  //9

4. 结构体嵌套和匿名成员

  1. go允许我们定义不带名称的结构体成员,只需要指定类型即可;这种结构体成员称作匿名成员。这个结构体成员的类型必须是一个命名类型或者指向命名类型的指针。正是因为有了这种结构体嵌套的功能,我们才能直接访问到我们需要的变量而不是指定一大串中间变量。
type Point struct {
    X int
    Y int
}
type Circle struct {
    Point
}
var c Circle
c.X = 10  //等价于 c.Point.X = 10
c.Y = 10  //等价于 c.Point.Y = 10

type Wheel struct {
    *Point
}
  1. 结构体字面量初始化没有快捷方式,必须遵循形状类型的定义。
type Point struct {
    X int
    Y int
}
type Circle struct {
    Point
}
var c Circle
c = Circle{1,1}  //错误
c = Circle{Point{1,1}}  //正确
c = Circle{Point: Point{1,1}}  //正确

3.因为“匿名成员”拥有隐式的名字,所以你不能在一个结构体里面定义两个相同类型的匿名成员,否则会引起冲突。由于匿名成员的名字是由它们的类型决定的,因此它们的可导出性也是由他们的的类型决定。在下面的例子中,point和circle这两个匿名成员是可导出的,即使这两个结构体是不可导出的(point和circle)。

type point struct {
    X int
    Y int
}
type circle struct {
    point
}

type Wheel struct {
    circle
}
var w Wheel
w.X = 8 // 等价于 w.circle.point.X = 8, w.X是可导出的,w.circle.point.X是不可导出的

5. 结构体方法

  1. 定义
    方法的声明和普通函数的声明类似,知识在函数名字前面多加了一个参数。这个参数把这个方法绑定到这个参数对应的类型上。
    附加的参数p成为方法的接收者, 它源于早先的面向对象语言,用来描述主调方法向对象发送消息。
type Point struct {
    X int
    Y int
}
func (p Point) print() {
    fmt.Println(p.X, p.Y)
}
  1. 方法定义限制
    go和许多其他面向对象的语言不同,它可以将方法绑定到除了指针类型和接口类型的任何类型上。可以很方便的为简单的类型(入整形、字符串、slice、map、甚至函数等)定义附加的行为。
//整形
type III int
func (i III) print() {
    fmt.Println(i)
}

//函数
func Test() {
    fmt.Println("test")
}
type FunT func()
func (f FunT) Print() {
    fmt.Println(f)
}
func main() {
    var f FunT
    f = Test
    f.Print()
}
  1. 指针接收者的方法
    由于主调函数会复制每一个实参变量,或者如果一个实参太大而我们希望避免复制整个实参,因此我们必须使用指针来传递变量的地址。这也同样适用于更新接收者我们将它绑定到指针类型。在调用方法的时候,编译器会对变量进行隐式转换。
    总结一下结构体方法可以成功调用的条件:
  • 实参接收者和形参接收者是同一类型,比如都是T或者都是*T。(1,4,5,7)
  • 实参接收者是T类型的变量而形参接收者是*T类型,编译器会隐式的获取变量的地址(3)。
  • 实参接收者是T类型而形参接收者是T类型,编译器会隐式的获取实际的取值。(2,6)
    其中8编译过程报错的原因是:编译器对T类型转化为
    T类型的隐式转化,只有实参接收者是变量才可以成功,因为无法获取临时变量的地址。
type Point struct {
    X int
    Y int
}
func (p Point) Print() {
    fmt.Println(p.X, p.Y)
}
func (p *Point) ScaleBy(factor int) {
    p.X *= factor
    p.Y *= factor
}
func main() {
    p := Point{1,1}
    ptr := &p
    p.Print()   //1. 正确
    ptr.Print() //2. 正确
    p.ScaleBy(2)      //3. 正确
    ptr.ScaleBy(2)    //4. 正确
    Point{1,1}.Print()    //5. 正确
    (&Point{1,1}).Print() //6. 正确
    (&Point{1,1}).ScaleBy( 2) //7. 正确
    Point{1,1}.ScaleBy( 2)    //8. 错误
}
  • nil是一个合法的接收者:就像一些函数允许nil指针作为实参,方法的接收者允许是nil指针
type A struct {
    Data int
}
func (a *A) FunPtrA() {
    fmt.Println("FunPtrA")
}
func main() {
    var ptrA *A
    ptrA = nil
    ptrA.FunPtrA()  //FunPtrA
}

4. 通过结构体内嵌组成类型
这里主要讨论内嵌结构体和结构体指针后,对于源结构体定义函数的访问权限。

type A struct {
    Data int
}
func (a A) FunA() {
    fmt.Println(a.Data)
}
func (a *A) FunPtrA1() {
    fmt.Println("FunPtrA1")
}
func (a *A) FunPtrA2() {
    fmt.Println("FunPtrA2")
    fmt.Println(a.Data)
}

type B struct {
    A
}
type C struct {
    *A
}
func main() {
    var b B
    b.FunA()    //可以访问
    b.FunPtrA1() //可以访问
    b.FunPtrA2() //可以访问

    var c C
    //c.FunA()     //不可访问
    c.FunPtrA1() //可以访问
    //c.FunPtrA2() //不可访问

    c.A = &A{}
    c.FunA()     //可以访问
    c.FunPtrA1() //可以访问
    c.FunPtrA2() //可以访问
}