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))
}