Go 语言结构体
基本概念
结构体(Struct)是一种自定义数据类型,由一系列相同或不同类型的数据组成,以实现较复杂的数据结构。Go 语言的结构体是值类型。
定义和初始化
结构体是自定义类型,因此在声明和初始化结构体变量前,需要显式定义结构体类型。这也是静态语言特色,所有数据在编译时都必须有明确类型。
定义结构体
结构体类型通过关键字 struct
来定义,内部可以包含多个字段(Field),每个字段都有独自类型和名称:
type StructName struct { Field1 FieldType1 Field2 FieldType2 // 更多字段... }
StructName
:结构体名称,虽然标识符首字母能决定是否导出,但通常以大写字母开头。Field1
:字段名称,通常也以大写字母开头,可选。字段名在结构体中必须唯一,可以使用空标识符。FieldType1
:字段类型,可以是任何有效类型,包括基本类型、函数、接口或者其他结构体类型。
此外,和定义常量组类似,相同类型字段可以定义在一起:
package main type Person struct { Name, City string // 相同类型定义在一起 Age int } func main() { }
初始化结构体
结构体变量称为结构体的实例或对象,访问实例字段用点号 .
作为操作符。声明并初始化结构体变量常用 3 种方式:
package main import "fmt" type Person struct { Name string Age int } func main() { // 声明后再赋值 var p1 Person // 自动初始化为类型零值 p1.Name = "Unknown" // 单独赋值 // 使用字面量初始化 p2 := Person{Name: "bob"} // 指定字段名赋值,允许部分赋值,不依赖赋值顺序 p3 := Person{"Bob", 11} // 省略字段名赋值,必须全部字段赋值,不能和键值对赋值混用 // 使用 new 函数初始化,获得指针 p4 := new(Person) // 等同于 p4 := &Person{} p4.Name, p4.Age = "Alice", 10 // 并行赋值 // 各种打印输出结构体方式 fmt.Println(p1.Name, p1.Age) // 输出:Unknown 0 fmt.Println(p2) // 输出:{bob 0} fmt.Printf("%+v\n", p3) // 输出:{Name:Bob Age:11} fmt.Printf("%#v", p4) // 输出:&main.Person{Name:"Alice", Age:10} }
结构体有时也通过工厂函数来初始化,在工厂函数中能封装错误检查或附加设置:
package main import "fmt" type Person struct { Name string Age uint } func NewPerson(name string, age uint) *Person { // 可以对输入参数做些额外检查 if age > 120 { panic("请输入正确参数") } return &Person{Name: name, Age: age} } func main() { p := NewPerson("Alice", 30) fmt.Println(p) }
嵌套结构体
结构体可以嵌套其他结构体和自定义类型,以创建更复杂的数据结构:
package main import "fmt" // BasicColor 结构体表示颜色 RGB 值 type BasicColor struct { Red, Green, Blue int } // AdvancedColor 结构体内嵌 BasicColor,并添加透明度 Alpha type AdvancedColor struct { BasicColor BasicColor Alpha float32 } func main() { color := AdvancedColor{} color.BasicColor.Red = 255 // 通过层级访问嵌套结构体字段 // 可以单独对内嵌结构体实例化,再引用赋值 bc := BasicColor{Green: 255} color = AdvancedColor{ BasicColor: bc, // 更美观更结构化 Alpha: 0.89, // 结尾逗号不能省略 } fmt.Printf("%+v\n", color) // 输出:{BasicColor:{Red:0 Green:255 Blue:0} Alpha:0.89} }
匿名结构体
匿名结构体没有类型名称,在定义的同时进行初始化,只能一次性使用:
package main import "fmt" func main() { // 直接使用匿名结构体定义变量 admin := struct { Id int Name string }{1, "admin"} fmt.Printf("%+v\n", admin) // 输出:{Id:1 Name:admin} fmt.Printf("%T\n", admin) // 输出:struct { Id int; Name string } // 在 new 函数中使用匿名结构体 user := new(struct { Id int Name string }) fmt.Printf("%+v\n", user) // 输出:&{Id:0 Name:} }
当结构体在极小作用域内使用时,可以使用匿名结构体来避免全局命名空间污染。
匿名字段
结构体可包含多个匿名字段,匿名字段只有类型没有命名,匿名字段名隐式等于类型名(导出也由类型名决定),所以每种数据类型只能有一个匿名字段,否则会命名冲突。匿名字段常用于嵌入其他结构体或接口,从而实现类似于继承的功能:
package main import "fmt" type Person struct { Name string Age int } type Employee struct { Person // 匿名字段 Salary int } // 三种赋值方式 func main() { // 命名字段赋值 e := Employee{ Salary: 5000, Person: Person{ Name: "John", Age: 30, }, } // 赋值时忽略字段名,要注意顺序 e = Employee{Person{"Cale", 30}, 6000} // 直接访问修改匿名字段属性 e.Age = 32 e.Person.Age = 31 fmt.Println(e.Name, e.Age, e.Salary) }
使用匿名字段可以简化调用名称。发生字段名冲突时,必须显式指定嵌入类型名来解决歧义:
package main import "fmt" type Person struct { Name string // Person 中 Name 字段表示真名 } type Employee struct { Person // 内嵌 Person 结构体,匿名字段 Name string // Employee 中 Name 字段表示职位 } func main() { // 忽略字段名快速初始化赋值 e := Employee{ Person{"Alice"}, "CEO", } // 分别访问两个 Name 字段值 fmt.Println(e.Person.Name, e.Name) // 输出:Alice CEO }
结构体应用
结构体一般会绑定方法来使用,这里列举一些方法以外的应用。
比较和赋值
相同类型结构体之间可以直接比较和赋值:
package main import "fmt" type Person struct { Name string Age int } func main() { // 初始化两个 Person 类型实例 p1 := Person{Name: "Alice", Age: 20} p2 := Person{} // 结构体相同可以直接赋值 p2 = p1 // 比较结构体实例内容 fmt.Println(p1 == p2) // 输出:true }
注意,如果结构体类型不同,操作会报错。包括有完全相同字段但命名不同的结构体类型之间,也无法进行比较赋值。匿名结构体则可以忽略类型名,直接比较字段,类似无类型常量:
package main import "fmt" type Person struct { Name string Age int } type Man struct { Name string Age int } func main() { // 结构体字段和内容相同,只有类型名不同 p1 := Person{Name: "Alice", Age: 20} p2 := Man{"Alice", 20} //fmt.Println(p1 == p2) // 无法直接比较和赋值,类型不同 // 同样数据结构匿名结构体 p3 := struct { Name string Age int }{"Alice", 20} // 可以比较 fmt.Println(p2 == p3 || p1 == p3) // 输出:true // 也可以互相赋值 p1, p3 = p3, p2 }
结构体转换
上面提到,如果两个不同名字结构体类型具有相同的字段名、字段类型和字段顺序,依然是不同类型,不能直接互相赋值和比较。但它们的值可以互相转换,类似不同长度整型间转换一样直接:
package main import "fmt" type Person struct{ Name string } type Man struct{ Name string } func main() { // 结构体转换,结构体类型名加要转换的类型 p1 := Person{Name: "Alice"} p2 := Man(p1) fmt.Println(p1, p2) fmt.Printf("%T %T", p1, p2) // 输出不同类型:main.Person main.Man }
传递结构体
由于结构体成员可以是引用类型数据,因此传递结构体时并非传递数据完整副本。值类型数据会创建副本,引用类型数据则保持引用特性,指向原始数据:
package main import "fmt" type Person struct { ID int Names []string } func main() { // 结构体中包含值类型和引用类型 p1 := Person{Names: []string{"Alice"}} fmt.Println("原始数据:", p1) p2 := p1 // 赋值时发生值传递 p1.ID = 1 // 修改值类型字段 p1.Names[0] = "Malice" // 修改引用类型字段 fmt.Println("原始数据修改后:", p1) // 两个字段都有修改 fmt.Println("副本数据跟着变:", p2) // 值类型字段不受影响,引用类型字段跟着修改 // 结构体指针类型 p3 := &p1 p3.ID = 2 // 修改指针类型字段 fmt.Println("指针副本数据:", p3) }
因此,在函数中需要修改带引用类型的结构体时,最好手动创建结构体完全副本,在副本上修改再返回,避免函数副作用。
结构体标签
结构体类型还能为结构体的字段添加元信息标签(tag),标签用于为数据交换格式(json、xml、yaml)提供序列化、反序列化、验证等信息。标签内容紧接字段定义后用反引号「`」括起来:
package main import ( "fmt" "reflect" ) type Person struct { Name string `json:"name,omitempty"` Age int `json:"age"` } func main() { bob := Person{"bob", 11} // 通过 Field 来索引结构体字段,获取 Tag 属性。输出:json:"name,omitempty" fmt.Println(reflect.TypeOf(bob).Field(0).Tag) }
上面 Name
字段标签指定转为 JSON 格式时,使用 name
作为该字段的键名,并且值为类型零值时忽略字段。
递归结构体
结构体可以在字段定义中引用自身(指针),以表示更复杂的层次或树状数据结构,如单向链表结构:
package main import "fmt" type ListNode struct { Value int // 存放有效数据 Next *ListNode // 指针指向后继节点 } // 函数递归搜索特定值 func search(head *ListNode, value int) *ListNode { if head == nil { return nil } if head.Value == value { return head } return search(head.Next, value) } func main() { // 创建链表的头节点 head := &ListNode{Value: 1} // 添加更多节点 second := &ListNode{Value: 2} third := &ListNode{Value: 3} head.Next = second second.Next = third // 搜索链表 fmt.Println(search(head, 0)) fmt.Println(search(head, 2)) }