Skip to main content

Struct

type

type关键字是一个主要对类型定义的工具:

  1. 给一个类型起一个别名,即类型别名。
  2. 创建一个新的类型,即自定义类型。
  3. type定义的类型名称首字母必须大写。
  4. type定义的类型名称不能与内置类型名称相同。

类型别名

类型别名是一个类型的另一种名称,本质上是同一种类型,主要让类型具备语义化。

内置类型中,runeint32的别名,byteuint8的别名。

package main

import "fmt"

type MyInt = int

func main() {
var a MyInt = 1
var b int = 1
fmt.Println(a == b)
}

类型别名可在给变量指定类型的时候使其更简洁一些:

type TypeCallback = func(
a int,
b int,
sign string,
res int,
) string

func compute(
a int,
b int,
method string,
cb TypeCallback,
) string {
// some code
}

自定义类型

自定义类型是一种被创建的真实的类型,且与内置类型是不同的类型:

package main

import "fmt"

type MyInt int

func main() {
var a MyInt = 1
var b int = 1

// mismatched types MyInt and int
fmt.Println(a == b)
}

自定义类型在增强语义化的同时,可起到运算隔离墙的作用,避免不同的类型进行运算:

package main

func main() {
type AccountBalance float64

var accountBalance AccountBalance
var income float64 = 123.12

// mismatched types AccountBalance and float64
total := accountBalance + income
}

区别

  1. 类型别名是为了对比较长的或者是需要语义化的类型取一个相对短的名称;自定义类型是创建了一种新的类型,有原来类型的全部特征,但又是一个独立的类型。
  2. 类型别名的类型还是等号右边的类型;自定义类型的类型就是新定义的类型。
  3. 如果仅仅是为了语义化类型定义,或者短化类型定义,那么就选择类型别名;如果一种类型的定义是复合型的、多元素的、复杂的,那么就选择自定义类型。

结构体

基本认识

Go语言中没有面向对象的设计思想,像classobject是没有的。结构体是类似于对象结构的数据类型,可以实现面向对象的一部分特征。

结构体描述的是一组数据的结构,是一种Go语言层面上的自定义类型,可以认为是多个字段的结构,封装多种类型的结构,使得字段名清晰、易读。

type StructName struct {
// struct fields
}

type Todo struct {
id int
text string
completed bool
}

初始化

初始化有多种方式方式,可根据场景使用:

// 方式一
todo := Todo{
id: 1,
text: "todo",
completed: false,
}

// 方式二 通过 new 初始化,返回一个指针
todo := new(Todo)
todo.id = 3
todo.text = "todo"
todo.completed = false

// 方式三
var todo Todo
todo.id = 1
todo.text = "todo"
todo.completed = false

缺省赋值

结构体可以不完整的进行赋值,如果对应字段没有赋值则使用默认值:

// text 使用默认值
todo2 := Todo{
id: 2,
completed: false,
}

修改

结构体也可以进行字段的修改:

todo.text = "This is a todo"

存储原理

结构体存储在一段连续的内存空间,初始默认分配4字节的空间,并按照字段的声明顺序依次分配空间,如果剩余的空间不够存储当前字段,则会继续分配4字节的连续空间。

结构体存储

由于会按照字段的声明顺序分配空间,因此不同的顺序可能会导致分配的总空间大小不同。

// unsafe.Sizeof(todo) => 12
type Todo struct {
a int8
b int32
c bool
}

// unsafe.Sizeof(todo) => 8
type Todo struct {
a int8
c bool
b int32
}

匿名结构体

匿名结构体可以进行临时的结构体声明:

package main

func main() {
myAddress := struct {
province string
city string
district string
detail string
}{
province: "陕西省",
city: "西安市",
district: "曲江新区",
detail: "曲江路1号",
}
}

结构体嵌套

结构体可以进行嵌套,更好的增强灵活性:

package main

import "fmt"

func main() {
type Address struct {
province string
city string
district string
detail string
}

type LogisticsInfo struct {
orderId string
productName string
province string
// 嵌套另一个结构体
address Address
}

logisticsInfo := LogisticsInfo{
orderId: "12344343",
productName: "Max",
province: "北京市",
address: Address{
province: "陕西省",
city: "西安市",
district: "曲江新区",
detail: "曲江路1号",
},
}

logisticsInfo.address.city = "渭南市"

fmt.Println(logisticsInfo)
}

如上,如果需要访问或修改内层结构体的值,需要一层一层的访问,那么有没有一种办法可以直接访问呢?

通过访问logisticsInfo.city达到logisticsInfo.address.city的效果。此时可采取匿名字段的方式进行定义:

package main

import "fmt"

func main() {
type Address struct {
province string
city string
district string
detail string
}

type LogisticsInfo struct {
orderId string
productName string
province string
// 可简单的理解为将其平铺
Address
}

logisticsInfo := LogisticsInfo{
"12344343",
"Max",
"北京市",
Address{
province: "陕西省",
city: "西安市",
district: "曲江新区",
detail: "曲江路路1号",
},
}

logisticsInfo.city = "渭南市"
fmt.Println(logisticsInfo)
}

实际开发中,具名字段使用较多,因为在赋值过程中字段名给了明确的标志,可阅读性较强。

相等性判断

结构体在特定情况下可进行相等性判断:

  1. 如果字段类型存在非原始类型,则不能进行比较。
  2. 如果字段类型均为原始类型,则会比较每个字段的值是否相等,全部字段相等则相等,否则不相等。
  3. 如果存在嵌套结构体,则会递归按照比较规进行比较。
package main

import "fmt"

func main() {
type Student struct {
id int
name string
age uint8
}

stu1 := Student{
id: 1,
name: "Tom",
age: 12,
}

stu2 := Student{
id: 1,
name: "Tom",
age: 12,
}

fmt.Println(stu1 == stu2) // true
}

如上,stu1stu2全部字段均相同,结果为true。若将stu1age改为20,结果则为false

此时,如果给结构体增加一种非原始类型字段,则不能进行比较:

// invalid operation: stu1 == stu2 
// (struct containing []string cannot be compared)

type Student struct {
id int
name string
age uint8
hobbies []string
}

结构体绑定函数

Go语言不允许直接在结构体内声明方法,考虑如下结构体,如果想给结构体字段绑定对应的设置方法,该如何做?

type Todo struct {
id int
text string
completed bool

// setId() {}
}

要解决这个问题,可通过结构体绑定函数的方式进行。这种情况下函数需要有一个接收器,接受一个结构体:

// func (receiver) fnName(pararms)

func (todo *Todo) setId(id int) {
todo.id = id
}

func (todo *Todo) setText(text string) {
todo.text = text
}

func (todo *Todo) setCompleted(completed bool) {
todo.completed = completed
}

函数的结构体接收器接收的是结构体值,本质上是一种值传递,结构体的值会复制一份传递给结构体绑定的函数。

info

基本数据类型作为参数传递是值传递,会先将数据复制一份,然后将数据传递。

slicemapchannel三种类型,虽然在传递的时候没有显示的传递指针,但在Go语言的底层,会复制一份值的指针,然后传递,并且指针可以直接访问到值。

在Go语言中,指针是可以直接访问值的,这是系统为这个操作做了相应的底层实现,而其他的静态语言没有这个功能。

结构体在传递的时候是值传递,而非指针传递。因此,如上的设置函数结构体接收到的todo是副本,在修改的时候,不会影响原数据。

如果传进来的是todo的指针,在修改的时候会影响原数据。

func main() {
todo := Todo{
id: 1,
text: "todo",
completed: false,
}

todo.setId(100)
fmt.Println(todo)
}

如果给结构体绑定的函数内部仅仅是访问数据,就可以保持值传递;如果函数内部需要做字段值的操作,需要使用指针传递。

泛型结构体

结构体也可以使用泛型,使得结构体更灵活:

type S[T int | float64] struct {
a T
b T
}

方法绑定原理

在 Go 的语法里,方法知识普通函数多了一个接收者参数,编译器在语义分析阶段会把

func (p *Point) Move(dx, dy int) { 
p.X += dx
p.Y += dy
}

视作一个真正的顶级函数,大致为:

func (*Point).Move(p *Point, dx, dy int)
  • 方法声明里的 (p *Point) 只是语法糖,真正生成的符号表条目依旧是函数,第 0 个参数即接收者。
  • 在调用 pt.Move(3,4) 时,编译器会重写成 (*Point).Move(&pt, 3, 4),方法值 pt.Move 则退化为闭包,把接收者 &pt 绑定进去。

值接收者和指针接收者有所区别:

方面值接收者 func (v T) M()指针接收者 func (p *T) M()
方法调用时的实参传入值的拷贝传入地址 (指针)
能否修改原对象不能。对字段的修改只作用在副本中可以。对字段的修改影响原对象
隐式地址自动转换调用方是 *T 时,编译器自动解引用 → 值调用方是 T 时,编译器自动取地址 → 指针
结构体大小影响大结构体值拷贝代价高仅传递 8 字节指针,代价低
接口实现方法属于值类型 T 的方法集;T*T 都能调用方法属于 *T 的方法集;只有 *T 能调用
可用于 map key不影响;T 需满足可比较*T 作为 map key 需要指针可比较(地址值比较)
  • 需修改接收者状态 → 用指针接收者。
  • 结构体较大或包含 mutex/chan 等非可比较字段 → 用指针接收者。
  • 值本身很小且方法只读、不想在接口里必须用指针 → 值接收者也可。
  • 一旦某方法选择指针接收者,建议该类型所有方法都用指针接收者,保持一致,避免混淆。
package main

import "fmt"

type Counter struct{ N int }

// 值接收者
func (c Counter) Value() int {
return c.N
}

// 指针接收者
func (c *Counter) Inc() { c.N++ }

type Valuer interface{ Value() int }
type Incer interface{ Inc() }

func main() {
var c Counter // 值变量
var p = &c // 指针变量

c.Inc() // 自动取址调用 (*Counter).Inc(&c)
fmt.Println(c.N) // 1

fmt.Println(p.Value()) // 自动解引用调用 (Counter).Value(*p)

// 接口实现检查
var _ Valuer = c // OK
var _ Valuer = p // OK
// var _ Incer = c // 编译错误: Counter 没有 Inc (方法集不含指针接收者)
var _ Incer = p // OK
}