导航菜单

结构体(Struct)

结构体是 Go 语言中最重要的复合数据类型,它将不同类型的字段组合成一个有意义的整体。结构体是面向对象编程在 Go 中的基础——Go 通过结构体和方法实现封装,通过嵌套实现组合,而不是传统的类继承。

结构体的定义

使用 type 关键字和 struct 关键字定义结构体:

type Person struct {
    Name string
    Age  int
    Email string
}
结构体(Struct)

结构体是一种聚合数据类型,它将零个或多个任意类型的命名字段组合在一起。每个字段都有名称和类型。结构体是值类型,当赋值给新变量或作为函数参数时,会进行完整的值拷贝。

定义与初始化

方式一:使用字段名初始化(推荐)

type Person struct {
    Name  string
    Age   int
    Email string
}

// 指定字段名初始化(字段顺序无关)
p1 := Person{
    Name:  "Alice",
    Age:   30,
    Email: "alice@example.com",
}

// 只初始化部分字段,其余为零值
p2 := Person{
    Name: "Bob",
    Age:  25,
}
fmt.Printf("%+v\n", p2) // {Name:Bob Age:25 Email:}

方式二:按顺序初始化(不推荐)

// 按字段声明顺序初始化(不推荐,容易出错)
p3 := Person{"Charlie", 35, "charlie@example.com"}

方式三:使用 new() 函数

// new(T) 分配一个 T 类型的零值,返回 *T
p4 := new(Person)
// p4 的类型是 *Person(指针)
// p4 指向的内存中,所有字段都是零值
fmt.Printf("%T\n", p4)  // *Person
fmt.Printf("%+v\n", *p4) // {Name: Age:0 Email:}

// 通过指针访问字段(Go 允许直接用 . 访问,不需要 ->)
p4.Name = "Dave"
p4.Age = 28
fmt.Printf("%+v\n", *p4) // {Name:Dave Age:28 Email:}

方式四:使用取地址操作符

p5 := &Person{} // 等价于 new(Person)
p5.Name = "Eve"
p5.Age = 22
new() 与字面量初始化的区别
  • new(Person) 创建一个零值的 *Person,所有字段为零值
  • &Person{Name: "Alice"} 创建一个部分初始化的 *Person
  • Person{Name: "Alice"} 创建一个值类型的 Person(栈上或逃逸到堆上)

在 Go 中,&Person{Name: "Alice"} 是更常用的方式,因为它可以在创建时就初始化字段。

方式五:零值初始化

var p Person // 所有字段初始化为零值
fmt.Printf("%+v\n", p) // {Name: Age:0 Email:}

访问和修改字段

使用点号 . 访问结构体字段,无论变量是值类型还是指针类型:

type Point struct {
    X, Y float64
}

p := Point{3.0, 4.0}

// 访问字段
fmt.Println(p.X) // 3
fmt.Println(p.Y) // 4

// 修改字段
p.X = 10.0
p.Y = 20.0

// 通过指针访问(Go 自动解引用,不需要 -> 操作符)
pp := &p
pp.X = 100.0
fmt.Println(p.X) // 100 —— p 也被修改了

匿名结构体

匿名结构体没有类型名称,适用于一次性使用的场景:

// 定义并初始化匿名结构体
person := struct {
    Name string
    Age  int
}{
    Name: "Anonymous",
    Age:  99,
}
fmt.Printf("%+v\n", person) // {Name:Anonymous Age:99}

匿名结构体常见使用场景:

// 场景一:函数返回多个值时,用匿名结构体组织
func getServerConfig() struct {
    Host string
    Port int
} {
    return struct {
        Host string
        Port int
    }{
        Host: "localhost",
        Port: 8080,
    }
}

// 场景二:表驱动测试中定义测试用例
tests := []struct {
    name     string
    input    int
    expected int
}{
    {"positive", 5, 25},
    {"zero", 0, 0},
    {"negative", -3, 9},
}

for _, tc := range tests {
    result := tc.input * tc.input
    if result != tc.expected {
        fmt.Printf("%s failed: got %d, want %d\n", tc.name, result, tc.expected)
    }
}

嵌套结构体

Go 通过结构体嵌套实现组合(Composition),而非传统的继承(Inheritance)。

具名嵌套

type Address struct {
    City    string
    Country string
    ZipCode string
}

type Person struct {
    Name    string
    Age     int
    Address Address // 具名嵌套
}

p := Person{
    Name: "Alice",
    Age:  30,
    Address: Address{
        City:    "Beijing",
        Country: "China",
        ZipCode: "100000",
    },
}

// 访问嵌套字段
fmt.Println(p.Address.City)    // Beijing
fmt.Println(p.Address.Country) // China

匿名嵌套(字段提升)

匿名嵌套与字段提升(Promoted Fields)

当结构体嵌套一个匿名结构体(或匿名接口)时,内层结构体的字段会被”提升”到外层。可以直接通过外层结构体访问内层的字段,无需写出中间层级。这类似于面向对象中的继承,但本质上是语法糖。

type Address struct {
    City    string
    Country string
}

type Person struct {
    Name string
    Age  int
    Address // 匿名嵌套(不写字段名)
}

p := Person{
    Name: "Alice",
    Age:  30,
    Address: Address{
        City:    "Shanghai",
        Country: "China",
    },
}

// 字段提升:可以直接访问 City,无需 p.Address.City
fmt.Println(p.City)    // Shanghai —— 等价于 p.Address.City
fmt.Println(p.Country) // China   —— 等价于 p.Address.Country

// 仍然可以通过完整路径访问
fmt.Println(p.Address.City) // Shanghai

// 如果外层和内层有同名字段,外层字段优先
type Employee struct {
    Name    string
    Address Address
    Name    string // 编译错误:重复字段名
}
type Skill struct {
    Name string
    Level int
}

type Hobby struct {
    Name string
    Since int
}

type Person struct {
    Name string
    Skill // 匿名嵌套:有 Name 字段
    Hobby // 匿名嵌套:也有 Name 字段
}

p := Person{
    Name: "Alice",
    Skill: Skill{Name: "Go", Level: 5},
    Hobby: Hobby{Name: "Chess", Since: 2010},
}

fmt.Println(p.Name) // "Alice" —— 外层字段优先

// p.Name 是有歧义的(Skill.Name 和 Hobby.Name 都被提升)
// 必须显式指定
fmt.Println(p.Skill.Name) // "Go"
fmt.Println(p.Hobby.Name) // "Chess"

组合 vs 继承

Go 使用组合替代继承,这是 Go 设计哲学的核心之一:

// 继承(Go 不支持):
//   class Dog extends Animal { ... }

// 组合(Go 的方式):
type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return "..."
}

type Dog struct {
    Animal     // 匿名嵌套 = "继承" Animal 的字段和方法
    Breed string
}

// 可以重写"父类"方法
func (d Dog) Speak() string {
    return "Woof!"
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Buddy"},
        Breed:  "Golden Retriever",
    }

    fmt.Println(d.Name)   // "Buddy" —— 字段提升
    fmt.Println(d.Speak()) // "Woof!" —— 方法重写
}

结构体的比较

可比较的结构体

如果结构体的所有字段都是可比较的,那么结构体本身也是可比较的。可以使用 ==!= 进行比较(逐字段比较):

type Point struct {
    X, Y int
}

p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{1, 3}

fmt.Println(p1 == p2) // true  —— 所有字段都相等
fmt.Println(p1 == p3) // false —— Y 字段不同

不可比较的结构体

如果结构体包含不可比较的字段(slice、map、func),则整个结构体不可比较:

type Data struct {
    Name  string
    Items []int // slice 不可比较
}

d1 := Data{Name: "test", Items: []int{1, 2}}
d2 := Data{Name: "test", Items: []int{1, 2}}

// d1 == d2 // 编译错误:struct containing []int cannot be compared

结构体标签(Struct Tags)

结构体标签(Struct Tags)

结构体标签是写在字段声明后面的反引号字符串,通常用于存储元数据。标签不会影响程序的逻辑行为,但可以通过反射(reflection)在运行时读取。最常见的用途是控制 JSON、YAML、数据库等格式的序列化和反序列化。

基本语法

type User struct {
    ID       int    `json:"id" db:"user_id"`
    Username string `json:"username" db:"username"`
    Email    string `json:"email" db:"email"`
    Password string `json:"-" db:"password"`           // json 中忽略此字段
    Age      int    `json:"age,omitempty" db:"age"`    // 零值时忽略
    CreatedAt string `json:"created_at" db:"created_at"`
}

JSON 标签详解

import "encoding/json"

type Article struct {
    Title     string `json:"title"`                  // 标准映射
    Author    string `json:"author,omitempty"`        // 零值时忽略
    Content   string `json:"content,omitempty"`
    Secret    string `json:"-"`                       // 完全忽略
    Published bool   `json:"published,string"`        // 输出为字符串
    Views     int    `json:"views,omitempty,string"`  // 零值忽略,输出为字符串
    Tags      []string `json:"tags,omitempty"`        // slice 零值为 nil 时忽略
}

func main() {
    a := Article{
        Title:     "Go Struct Tags",
        Author:    "",
        Content:   "Hello, World!",
        Secret:    "hidden",
        Published: true,
        Views:     0,
        Tags:      nil,
    }

    data, _ := json.MarshalIndent(a, "", "  ")
    fmt.Println(string(data))
    // 输出(Secret 被忽略,Author 为空字符串被忽略,Views 为 0 被忽略,Tags 为 nil 被忽略):
    // {
    //   "title": "Go Struct Tags",
    //   "content": "Hello, World!",
    //   "published": "true",
    //   "tags": []
    // }
    // 注意:Tags 为 nil 被 omitempty 忽略,但 []string{} 空切片不会被忽略

    // 反序列化
    jsonStr := `{"title":"Learn Go","author":"Alice","extra":"ignored"}`
    var a2 Article
    json.Unmarshal([]byte(jsonStr), &a2)
    fmt.Printf("%+v\n", a2)
    // {Title:Learn Go Author:Alice Content: Secret: Published:false Views:0 Tags:[]}
    // extra 字段被忽略(不会报错)
}

常用标签选项汇总

标签选项说明
json:"field_name"基本映射JSON 键名
json:"field_name,omitempty"零值忽略字段为零值时不输出
json:"-"忽略序列化和反序列化都忽略此字段
json:"-,"强制输出空名输出 "" 作为键名
json:"field,string"字符串化数值/布尔值输出为 JSON 字符串
json:",omitempty,string"组合零值忽略 + 字符串化
db:"column_name"数据库列名常用于 GORM、sqlx 等 ORM
yaml:"field_name"YAML 映射常用于配置文件解析
binding:"required"验证规则常用于 Gin 框架的参数验证
validate:"min=1,max=100"验证规则常用于 validator 库

通过反射读取标签

import (
    "fmt"
    "reflect"
)

type Config struct {
    Host string `json:"host" yaml:"host" env:"HOST" required:"true"`
    Port int    `json:"port" yaml:"port" env:"PORT" required:"true"`
}

func main() {
    t := reflect.TypeOf(Config{})

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("字段名: %s\n", field.Name)
        fmt.Printf("  json tag: %s\n", field.Tag.Get("json"))
        fmt.Printf("  yaml tag: %s\n", field.Tag.Get("yaml"))
        fmt.Printf("  env  tag: %s\n", field.Tag.Get("env"))
        fmt.Printf("  required:  %s\n", field.Tag.Get("required"))
    }
    // 字段名: Host
    //   json tag: host
    //   yaml tag: host
    //   env  tag: HOST
    //   required:  true
    // ...
}

new() 与字面量初始化的区别

type Student struct {
    Name string
    Age  int
}

// 方式一:new()
s1 := new(Student) // 返回 *Student,所有字段为零值

// 方式二:取地址
s2 := &Student{}  // 返回 *Student,所有字段为零值(与 new 等价)

// 方式三:取地址并初始化
s3 := &Student{Name: "Alice", Age: 20} // 返回 *Student,字段已初始化

// 方式四:值类型字面量
s4 := Student{Name: "Bob", Age: 21} // 返回 Student(值类型)
方式返回类型字段值使用场景
new(Student)*Student全部零值兼容旧代码,较少使用
&Student{}*Student全部零值需要指针时的常用写法
&Student{Name: "A"}*Student按指定初始化需要指针且要初始化字段
Student{Name: "A"}Student按指定初始化不需要指针时推荐

练习题

练习 1:结构体定义与方法

定义一个 Rectangle 结构体,包含 WidthHeight 两个字段。为其添加 Area()Perimeter() 方法,分别计算面积和周长。

参考答案
package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func main() {
    r := Rectangle{Width: 10, Height: 5}
    fmt.Printf("面积: %.2f\n", r.Area())       // 面积: 50.00
    fmt.Printf("周长: %.2f\n", r.Perimeter())   // 周长: 30.00
}

练习 2:嵌套结构体与 JSON

定义一个 Employee 结构体,嵌套 Address 结构体。使用 JSON 标签将以下 JSON 反序列化为该结构体,并打印结果:

{
  "name": "Alice",
  "age": 30,
  "address": {
    "city": "Beijing",
    "country": "China"
  },
  "salary": 50000
}
参考答案
package main

import (
    "encoding/json"
    "fmt"
)

type Address struct {
    City    string `json:"city"`
    Country string `json:"country"`
}

type Employee struct {
    Name    string  `json:"name"`
    Age     int     `json:"age"`
    Address Address `json:"address"`
    Salary  int     `json:"salary"`
}

func main() {
    jsonStr := `{
        "name": "Alice",
        "age": 30,
        "address": {
            "city": "Beijing",
            "country": "China"
        },
        "salary": 50000
    }`

    var emp Employee
    if err := json.Unmarshal([]byte(jsonStr), &emp); err != nil {
        fmt.Println("解析错误:", err)
        return
    }

    fmt.Printf("%+v\n", emp)
    // {Name:Alice Age:30 Address:{City:Beijing Country:China} Salary:50000}

    // 重新序列化
    data, _ := json.MarshalIndent(emp, "", "  ")
    fmt.Println(string(data))
}

练习 3:结构体比较

编写一个程序,验证两个结构体变量的比较行为:一个所有字段都是可比较类型的结构体和一个包含 slice 字段的结构体,分别尝试用 == 比较。

参考答案
package main

import "fmt"

type Comparable struct {
    X int
    Y int
}

type NotComparable struct {
    Name  string
    Items []int
}

func main() {
    a := Comparable{X: 1, Y: 2}
    b := Comparable{X: 1, Y: 2}
    c := Comparable{X: 1, Y: 3}

    fmt.Println(a == b) // true —— 所有字段相等
    fmt.Println(a == c) // false —— Y 字段不同

    m := NotComparable{Name: "test", Items: []int{1, 2}}
    n := NotComparable{Name: "test", Items: []int{1, 2}}

    // m == n // 编译错误:struct containing []int cannot be compared

    // 不可比较的结构体可以使用 reflect.DeepEqual
    // import "reflect"
    // fmt.Println(reflect.DeepEqual(m, n)) // true
}

搜索