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