struct

Written by with ♥ on in Go

通过使用结构体,我们可以从语言的基本类型构造出更复杂的抽象类型,降低软件开发复杂度。

struct 是值类型,赋值和传参都会复制。

可⽤ "_" 定义补位字段,支持指向自身类型的指针成员。

支持 ==、!= 比较操作符。

type Node struct {
    _ int
    id int
    data *byte
    next *Node
}

type User struct {
	id int
	name string
}

声明

// 所有字段初始化为零值
var user User

// 字面值初始化
user := User{1, "Tom"}

// 空结构体
// 大小为 0 不包含任何字段
empty := struct{}{}

指针

// 取 usre 变量的地址
point := &User{1, "Tom"}

// 取 user 变量 id 字段的地址
point := &user.id

::: tip t := &T{} 只是语法糖,真正执行的语句是 tmp := T{}; t = &tmp。 :::

访问

通过点操作符访问结构体的字段。

// 访问 user 的 id 字段
user.id

// 通过 user 指针访问 id 字段
// 等于 (*point).id 语句
// (*point) 间接取值,取出指针所指向的 user 变量值
point.id

匿名字段

匿名字段本质上是一种语法糖,只是一个与嵌入字段类型同名的字段。被匿名嵌入的可以是任何类型,也包括指针。

type User struct {
    name string
}

type Manager struct {
    User // 匿名字段
    title string
}

m := Manager{
    User: User{"Tom"}, // 匿名字段的显式字段名,和类型名相同。
    title: "Administrator",
}

m.name = "jack" // 访问匿名字段成员

可以像访问普通字段一样访问匿名字段成员,编译器从外向内逐级查找所有层次的匿名字段,直到发现目标或出错。

外层同名字段会遮蔽嵌⼊字段成员,相同层次的同名字段也会让编译器⽆所适从。解决⽅法是使⽤显式字段名。

本质上就是不能有歧义,不能同时找到两个,而不知道使用哪一个。

面向对象

面向对象三大特征里,Go 仅支持封装,尽管匿名字段的内存布局和行为类似继承。没有 class 关键字,没有继承、多态等等。

内存布局和 C struct 相同,没有任何附加的 object 信息。

type User struct {
    id int
    name string
}

type Manager struct {
    User
    title string
}

m := Manager{User{1, "Tom"}, "Administrator"}

// var u User = m // Error: cannot use m (type Manager) as type User in assignment. 没有继承自然没有多态

var u User = m.User // 同类型拷贝

内存布局

    |<-------- User:24 ------->|<-- title:16 -->|
    +-------------+------------+----------------+              +---------------+
 m  |      int      |    string  |     string     |              | Administrator | [n]byte 
    +-------------+------------+----------------+              +---------------+
                        |               |                              |
                        |               +--->>>------------->>>--------+
                        |
                        +--->>>----------------------------------------+
                                                                       |
                        +--->>>-------------------------------------+  |
                        |                                           |  |
    +-------------+------------+                                 +-------------+
 u  |     int       |    string  |                                 |    Tom      |  [n]byte
    +-------------+------------+                                 +-------------+                                  |<-   | <- id:8  ->|<- name:16->|                                      

方法

方法签名定义方法的名字,参数,返回值。

方法总是绑定对象实例,并隐式将实例作为第一实参(receiver)。

  • 只能为当前包内命名类型定义⽅法
  • 参数 receiver 可任意命名。如⽅法中未曾使⽤,可省略参数名
  • 参数 receiver 类型可以是 T*T。基类型 T 不能是接⼝或指针
  • 不⽀持⽅法重载,receiver 只是参数签名的组成部分
  • 可⽤实例 valuepointer 调⽤全部⽅法,编译器⾃动转换

没有构造和析构⽅法,通常⽤简单⼯⼚模式返回对象实例。

type Queue struct {
    elements []interface{}
}

// 创建对象实例
func NewQueue() *Queue {
    return &Queue{make([]interface{}, 10)}
}

// 省略 receiver 参数名
func (*Queue) Push(e interface{}) error { 
    panic("not implemented")
}

// Error: method redeclared: Queue.Push
// func (Queue) Push(e int) error {
//      panic("not implemented")
// }

// receiver 参数名可以是 self、this 或其他
func (self *Queue) length() int {
    return len(self.elements)
}

⽅法不过是⼀种特殊的函数,只需将其还原,就知道 receiver T 和 *T 的差别。

// 还原后的方法
func Push(receiver *Queue, e interface{}) error { 
    panic("not implemented")
}

匿名字段

可以直接访问匿名字段的方法,编译器负责查找。这个特性相当于变相实现了面向对象部分继承的能力。

并且,根据编辑器查找次序,只需在外层定义同名方法,就可以实现某种意义上的 override

type User struct {
    id int
    name string
}

type Manager struct {
    User
    title string    
}

func (self *User) ToString() string {
    return fmt.Sprintf("User: %p, %v", self, self)
}

func (self *Manager) ToString() string {
    return fmt.Sprintf("Manager: %p, %v", self, self)
}

func main() {
    m := Manager{User{1, "Tom"}, "Administrator"}
    fmt.Println(m.ToString())
    fmt.Println(m.User.ToString())
}

输出

Manager: 0x2102271b0, &{{1 Tom} Administrator}
User : 0x2102271b0, &{1 Tom}

方法集

每个类型都有与之关联的⽅法集,这会影响到接⼝实现规则。

  • 类型 T 方法集包含全部 receiver T 方法
  • 类型 *T ⽅法集包含全部 receiver T + *T ⽅法
  • 如果类型 S 包含匿名字段 T,则 S ⽅法集包含 T ⽅法
  • 如果类型 S 包含匿名字段 *T,则 S ⽅法集包含 T + *T ⽅法
  • 不管嵌⼊ T*T*S ⽅法集总是包含 T + *T ⽅法

用对象实例的 valuepointer 调用方法(包括匿名方法)不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 参数。

表达式

根据调用者不同,方法分为两种形式

instance.method(args...) 这种方式绑定实例,称为 method value

<type>.func(instance, args...) 这种方式必须显式传参,称为 method expression

type User struct {
    id int
    name string
}

func (self User) TestValue() {
    fmt.Printf("TestValue: %p, %v\n", &self, self)
}

func (self *User) TestPointer() {
    fmt.Printf("TestPointer: %p, %v\n", self, self)
}

func main() {
    u := User{1, "Tom"}
    
    mValue := u.Test // 隐式传递 receiver,⽴即复制 receiver,因为不是指针类型,不受后续修改影响。
    
    u.id, u.name = 2, "Jack"
    u.Test()
 
    mValue()

    mValueType = User.TestValue // User 方法集包含 TestValue 方法
                                // 签名变为 func TestValue(self User)
                                // receiver value 复制
    mValueType(u) // 显式传递 receiver

    mp := (*User).TestPointer // *User 方法集包含 TestPointer 方法
    
    mp(&u)

    mPoniterType = (*User).TestValue // *User 方法集包含 TestPointer 方法

    mPoniterType(&u) // 这里传的是 user 的地址,函数签名变成 func TestValue(self *User),实际也是 receiver value 复制
}

在汇编层⾯,method value 和闭包的实现⽅式相同,实际返回 FuncVal 类型对象。

FuncVal { method_address, receiver_copy }

再来一个例子,将方法还原成函数,就好理解了。

type Data struct{}

func (Data) TestValue() {}

func (*Data) TestPointer() {}

func main() {
    var p *Data = nil
    
    p.TestPointer()
    
    (*Data)(nil).TestPointer() // method value
    
    (*Data).TestPointer(nil) // method expression
    
    // p.TestValue() // invalid memory address or nil pointer dereference, nil 不能作为非指针 receiver 的值,只能作为结构指针的值

    // (Data)(nil).TestValue() // cannot convert nil to type Data,隐式传递 nil 为 receiver,类型不匹配
    
    // Data.TestValue(nil) // cannot use nil as type Data in function argument
}

value receiver And pointer receiver

Value receiver:

func (u user) fun1() {
    ....
}

Pointer receiver:

func (u *user) fun2() {
    ....
}

Value receiver 操作的是值的拷贝,而 Pointer receiver 操作的是实际的值。

pointer 去调用 value receiver 的方法,实际的操作是:

// 取值调用
(*p).fun1()

而用 value 去调用 pointer receiver 的方法,实际的操作是:

// 取址调用
(&v).fun2()