go语言结构体

通用的、对实体对象进行聚合抽象的能力。
在 Go 中,提供这种聚合抽象能力的类型是结构体类型,也就是 struct。

(1) 结构体使用

(1.1) 定义一个结构体

定义一个User struct

type User struct {
	Id       int64
	UserName string
	age      int8
	address  string
}

空结构体

// Empty是一个不包含任何字段的空结构体类型
type Empty struct{

} 

空结构体类型有什么用呢?

var s Empty
println(unsafe.Sizeof(s)) // 0

输出的空结构体类型变量的大小为 0,也就是说,空结构体类型变量的内存占用为 0。

 

(1.2) 初始化

零值初始化、复合字面值初始化,以及使用特定构造函数进行初始化

零值初始化

func main() {
	u := User{}
	fmt.Println(u) // {0  0 }
}
func main() {
	u2 := User{
		Id:       int64(1),
		UserName: "李四",
		age:      20,
		address:  "北京海淀",
	}
  fmt.Println(u2) // {1 李四 20 北京海淀}
} 

(1.3) 结构体类型的类型嵌入

type S struct {
    A int
    b string
    c T
    p *P
    _ [10]int8
    F func()
}

type T1 int
type t2 struct{
    n int
    m int
}

type I interface {
    M1()
}

type S1 struct {
    T1
    *t2
    I            
    a int
    b string
}

这种以某个类型名、类型的指针类型名或接口类型名,直接作为结构体字段的方式就叫做结构体的类型嵌入,这些字段也被叫做嵌入字段(Embedded Field)。

类型嵌入这种看似“继承”的机制,实际上是一种组合的思想。更具体点,它是一种组合中的代理(delegate)模式。

 

(2) 结构体类型的内存布局

var t T
unsafe.Sizeof(t)      // 结构体类型变量占用的内存大小
unsafe.Offsetof(t.Fn) // 字段Fn在内存中相对于变量t起始地址的偏移量

(2.1) 内存对齐

为什么会出现内存对齐的要求呢?这是出于对处理器存取数据效率的考虑。在早期的一些处理器中,比如 Sun 公司的 Sparc 处理器仅支持内存对齐的地址,如果它遇到没有对齐的内存地址,会引发段错误,导致程序崩溃。我们常见的 x86-64 架构处理器虽然处理未对齐的内存地址不会出现段错误,但数据的存取性能也会受到影响。

下面两个结构体类型表示的抽象是相同的,但正是因为字段排列顺序不同,导致它们的大小也不同:

type T struct {
    b byte
    i int64
    u uint16
}

type S struct {
    b byte
    u uint16
    i int64
}

func main() {
    var t T
    println(unsafe.Sizeof(t)) // 24
    var s S
    println(unsafe.Sizeof(s)) // 16
}

在日常定义结构体时,一定要注意结构体中字段顺序,尽量合理排序,降低结构体对内存空间的占用。

 

(3) 基本类型和指针类型

基本类型的struct和指针类型的struct有什么区别
下面以 Dog 和 *Dog 为例

(3.1) 基本类型

package main

import "fmt"

// Dog
type Dog struct {
	Name string
	Age  int
}

func (d Dog) setName(name string) {
	d.Name = name
	fmt.Println("第二次打印dog ", d)
}

func main() {
	dog := Dog{Name: "dog1", Age: 1}
	fmt.Println("第一次打印dog ", dog)
	dog.setName("dog2")
	fmt.Println("第三次打印dog ", dog)
}
第一次打印dog  {dog1 1}
第二次打印dog  {dog2 1}
第三次打印dog  {dog1 1}

(3.2) 指针类型

package main

import "fmt"

// Dog
type Dog struct {
	Name string
	Age  int
}

func (d *Dog) setName(name string) {
	d.Name = name
	fmt.Println("第二次打印dog ", d)
}

func main() {
	dog := Dog{Name: "dog1", Age: 1}
	fmt.Println("第一次打印dog ", dog)
	dog.setName("dog2")
	fmt.Println("第三次打印dog ", dog)
}
第一次打印dog  {dog1 1}
第二次打印dog  &{dog2 1}
第三次打印dog  {dog2 1}

Dog左边再加个代表的就是Dog类型的指针类型。这时,Dog可以被叫做Dog的基本类型。
可以对比基本类型和指针类型的日志,指针类型修改完后

(3.2) 值方法和指针方法之间有什么不同点呢?

它们的不同如下所示。
1、值方法的接收者是该方法所属的那个类型值的一个副本。
我们在该方法内对该副本的修改一般都不会体现在原值上,除非这个类型本身是某个引用类型(比如切片或字典)的别名类型。
而指针方法的接收者,是该方法所属的那个基本类型值的指针值的一个副本。我们在这样的方法内对该副本指向的值进行修改,却一定会体现在原值上。
2、一个自定义数据类型的方法集合中仅会包含它的所有值方法,而该类型的指针类型的方法集合却囊括了前者的所有方法,包括所有值方法和所有指针方法。
严格来讲,我们在这样的基本类型的值上只能调用到它的值方法。但是,Go 语言会适时地为我们进行自动地转译,使得我们在这样的值上也能调用到它的指针方法。比如,在Cat类型的变量cat之上,之所以我们可以通过cat.SetName(“monster”)修改猫的名字,是因为 Go 语言把它自动转译为了(&cat).SetName(“monster”),即:先取cat的指针值,然后在该指针值上调用SetName方法。
3、一个类型的方法集合中有哪些方法与它能实现哪些接口类型是息息相关的。
如果一个基本类型和它的指针类型的方法集合是不同的,那么它们具体实现的接口类型的数量就也会有差异,除非这两个数量都是零。比如,一个指针类型实现了某某接口类型,但它的基本类型却不一定能够作为该接口的实现类型。

(4) 思考

Go 语言不支持在结构体类型定义中,递归地放入其自身类型字段,但却可以拥有自身类型的指针类型、以自身类型为元素类型的切片类型,以及以自身类型作为 value 类型的 map 类型的字段,你能思考一下其中的原因吗?

一个类型,它所占用的大小是固定的,因此一个结构体定义好的时候,其大小是固定的。

但是,如果结构体里面套结构体,那么在计算该结构体占用大小的时候,就会成死循环。

但如果是指针、切片、map等类型,其本质都是一个int大小(指针,4字节或者8字节,与操作系统有关),因此该结构体的大小是固定的,记得老师前几节课讲类型的时候说过,类型就能决定内存占用的大小。

因此,结构体是可以接口自身类型的指针类型、以自身类型为元素类型的切片类型,以及以自身类型作为 value 类型的 map 类型的字段,而自己本身不行。

参考资料

[1] 实效Go编程
[2] 17|复合数据类型:用结构体建立对真实世界的抽象
[3] 13 | 结构体及其方法的使用法门
[4] Go语言基础之结构体
[5] 惊了!原来Go语言也有隐式转型