主页

索引

模块索引

搜索页面

3.1.1. struct结构

基本

字段

填充字段:

struct {
    x, y int
    _ float32  // 填充
    A *[]int
}

匿名字段:

struct {
    T1          // 字段名 T1
    *T2         // 字段名 T2
    P.T3        // 字段名 T3
    *P.T4       // 字段名T4
    x, y int    // 字段名 x  y
}

实例化:

var t T         // 给 t 分配内存,并零值化内存,这时 t 是类型T
t := new(T)     // 变量 t 是一个指向 T的指针, 即*T
var p *T   // p是指向一个结构体类型变量的指针

表达式 new(Type) 和 &Type{} 是等价的。
&struct1{a, b, c} 是一种简写,底层仍然会调用 new ()

origin := Point3D{}                             //  Point3D 是零值
line := Line{origin, Point3D{y: -4, z: 12.3}}   //   line.q.x 是零值

标签(tag)

标签的内容不可以在一般的编程中使用,只有 reflect 包能获取它:

type Student struct {
    name string "学生名字"          // 结构体标签
    Room int    `json:"Roomid"` // 结构体标签
}
func main() {
    st := Student{"Titan", 14}
    fmt.Println(reflect.TypeOf(st).Field(0).Tag)  // 学生名字
    fmt.Println(reflect.TypeOf(st).Field(1).Tag)  // json:"Roomid"
}

匿名成员

备注

Go语言结构体中可以包含一个或多个匿名(内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字(这一特征决定了在一个结构体中,每种数据类型只能有一个匿名字段)

嵌入与聚合

  1. 在接口中嵌入接口:

    相当于合并了两个接口类型定义的全部函数
    
  2. 在接口中嵌入结构体(不合法)

  3. 在结构体中内嵌接口:

    初始化的时候,内嵌接口要用一个实现此接口的结构体赋值
    这个新结构体可作为初始化时实现了内嵌接口的结构体来赋值
    
  4. 在结构体中嵌入结构体:

    不能嵌入自身值类型,可以嵌入自身的指针类型即递归嵌套
    在初始化时,内嵌结构体也进行赋值;外层结构自动获得内嵌结构体所有定义的字段和实现的方法
    

语法糖:

stu := new(Student)
fmt.Printf("%d\n", stu.name)    // 相当于: (*stu).name, 这是一个语法糖

内嵌结构体的字段:

stu.Human.name
如果外层结构体中没有同名的name字段,也可用: stu.name

命名冲突

当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢?外层名字会覆盖内层名字(但是两者的内存空间都保留)。 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误,但不使用没关系

下面代码中如果写成 c.a 是错误的:

type A struct {a int}
type B struct {a, b int}

type C struct {A; B}
var c C

fmt.Printf(c.A.a, c.B.a)    // ✅
fmt.Printf(c.a)             // 🚫

值接收者和指针接收者

纯结构体实现

在调用方法的时候:

值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;
指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法

备注

不管方法的接收者是什么类型,该类型的值和指针都可以调用

结构体类型:

type Person struct {
  age int
}

func (p Person) howOld() int {
  return p.age
}
func (p *Person) growUp() {
  p.age += 1
}
  1. 值类型:

    func main() {
      qcrao := Person{age: 18}
    
      // 值类型 调用接收者也是值类型的方法
      fmt.Println(qcrao.howOld())     // 18
    
      // 值类型 调用接收者是指针类型的方法
      qcrao.growUp()    // 语法糖1️⃣实际上调用的是: (&qcrao).growUp()
      fmt.Println(qcrao.howOld())     // 19
    }
    
  2. 指针类型:

    func main() {
      stefno := &Person{age: 100}
    
      // 指针类型 调用接收者是值类型的方法
      fmt.Println(stefno.howOld())    // 100: (语法糖2️⃣实际上调用的是: (*stefno).howOld())
    
      // 指针类型 调用接收者也是指针类型的方法
      stefno.growUp()
      fmt.Println(stefno.howOld())    // 101
    }
    

接口实现

定义接口:

type Human interface {
  howOld() int
  growUp()
}
  1. 指针类型请求值类型✅:

    func main() {
      var c Human = &Person{18}
      fmt.Println(c.howOld())
      c.growUp()
      fmt.Println(c.howOld())
    }
    
  2. 值类型请求指针类型🚫:

    func main() {
      var c Human = Person{18}
      fmt.Println(c.howOld())
      c.growUp()  // 🚫
      fmt.Println(c.howOld())
    }
    ./prog.go:23:11: cannot use Person literal (type Person) as type Human in assignment:
      Person does not implement Human (growUp method has pointer receiver)
    

备注

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

值类型&对象指针分别在何时使用

  • 如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者

  • 如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身

备注

使用指针作为方法的接收者的理由:1.方法能够修改接收者指向的值。2.避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

匿名结构体&匿名接口

sort包中有这么一个interface,实现了数组的大小比较:

type Interface interface {
    Less(i, j int) bool
}

// Array 实现Interface接口
type Array []int

func (arr Array) Less(i, j int) bool {
    return arr[i] < arr[j]
}

匿名结构体

上面实现了比较大小,如果第i个元素比第j个元素小返回true,现在想实现反过来的功能,即当第i元素小于第j个元素时返回false,则:

// 使用匿名struct
type reverse struct {
    Array
}

// 重写Less方法
func (r reverse) Less(i, j int) bool {
    return r.Array.Less(j, i)
}

// 构造reverse Interface
func Reverse(data Array) Interface {
    return &reverse{data}
}

使用:

func main() {
    arr := Array{1, 2, 3}
    rarr := Reverse(arr)
    fmt.Println(arr.Less(0, 1))
    fmt.Println(rarr.Less(0, 1))
}

匿名嵌入类型方法集提升的规则:

1. 如果 S 包含一个匿名字段 T,S 和 *S 的方法集都包含接收器为 T 的方法提升
2. 如果 S 包含一个匿名字段 T, *S 类型的方法集包含接收器为 *T 的方法提升
3. 如果 S 包含一个匿名字段 *T,S 和 *S 的方法集都包含接收器为 T 或者 *T 的方法提升

匿名接口

上面是 匿名接口体 的实现,现在来看看使用 匿名接口 的实现:

// 匿名接口(anonymous interface)
type reverse struct {
    Interface
}

// 重写(override)
func (r reverse) Less(i, j int) bool {
    return r.Interface.Less(j, i)
}

// 构造reverse Interface
func Reverse(data Interface) Interface {
    return &reverse{data}
}

使用:

func main() {
    arr := Array{1, 2, 3}
    rarr := Reverse(arr)
    fmt.Println(arr.Less(0,1))
    fmt.Println(rarr.Less(0,1))
}

总结

上面的2个实例看, 匿名接口匿名结构体 的实现非常相似。但是仔细对比一下你就会发现匿名接口的优点,匿名接口的方式不依赖具体实现,可以对任意实现了该接口的类型进行重写。这在写一些公共库时会非常有用,如果你经常看一些库的源码,匿名接口的写法应该会很眼熟。

对结构体添加一些约束

再增加一个Array2类型:

type Array2 []int

func (arr Array2) Less(i, j int) bool {
    return arr[i] < arr[j]
}

增加额外字段type:

type Sortable struct {
    Interface
    // other field
    Type string
}

func NewSortable(i Interface) Sortable {
    t := reflect.TypeOf(i).String()

    return Sortable{
        Interface: i,
        Type:      t,
    }
}

使用:

func DoSomething(s Sortable) {
    fmt.Println(s.Type)         // 打印为Array或Array2
    fmt.Println(s.Less(0, 1))   // 等同于s.Interface.Less(0, 1)
}

func main() {
    arr1 := Array1{1, 2, 3}
    arr2 := Array2{3, 2, 1, 0}

    DoSomething(NewSortable(arr1))
    DoSomething(NewSortable(arr2))
}

总结

参考

主页

索引

模块索引

搜索页面