Go 结构体 Struct — 把数据打包成对象

Go 没有"类"(class)这个关键字,但它有结构体(struct)——把几个相关字段打包在一起,组成一个新的类型。可以把它理解成一张"表格的字段定义":比如一个人有姓名、年龄、身高,定义一个 Person 结构体就能把它们装在一起。结构体是 Go 组织数据的核心,后面的"方法"和"接口"都建立在它之上。

一、定义结构体

type 名字 struct 来定义,后面跟一对花括号 { },里面列出每个字段的"名字 类型"。一行写一个字段,同类型的字段也可以合并在一行,比如 X, Y int。注意 Go 社区约定:字段名首字母大写表示对外公开(其它包能访问),小写则是私有的——这取代了 public/private 关键字。

package main

import "fmt"

// 定义结构体:把多个字段组合成一个新类型
type Person struct {
    Name string
    Age  int
}

func main() {
    // 三种常见实例化方式
    p1 := Person{Name: "小明", Age: 20}   // 按字段名赋值(推荐,顺序随意)
    p2 := Person{"小红", 18}              // 按声明顺序赋值(所有字段都要写)
    p3 := new(Person)                    // 返回 *Person,字段先初始化为零值
    p3.Name = "小刚"                     // 再单独赋值

    fmt.Println(p1, p2, p3)
    // {小明 20} {小红 18} &{小刚 0}

    // 字段访问与修改:点号
    p1.Age = 21
    fmt.Println(p1.Name, p1.Age)         // 小明 21
}

实例化主要有三种方式。按字段名 Person{Name:"小明", Age:20} 最推荐,清晰且不依赖顺序;按顺序 Person{"小红", 18} 简洁但所有字段都必须写、还得和声明顺序一致,容易出错;new(Person) 返回的是指针 *Person,字段先全部置为零值,再按需赋值。

二、字段访问与值类型

访问和修改字段都用点号 p.Namep.Age = 21。需要特别强调:和数组一样,结构体本身是值类型——把一个结构体赋值给另一个变量、或者当作参数传给函数,都会整体拷贝一份。修改副本不会影响原对象。如果你希望"传递的是引用",就用指针 *Person

三、嵌套与组合:Go 的"继承"

Go 刻意没有 class 继承,但它用组合达到了类似效果。做法很优雅:在一个结构体里,把另一个结构体作为匿名字段嵌进来(只写类型名、不写字段名)。这时内嵌结构体的字段会被自动"提升"——你可以像访问外层自己的字段一样,直接 u.City,而不用写 u.Address.City。这就是 Go 社区常说的"组合优于继承"。

package main

import "fmt"

// 内嵌基础结构
type Address struct {
    City   string
    Street string
}

// 用"匿名字段"把 Address 嵌进来,实现组合
type User struct {
    Name string
    Address        // 只写类型名,不写字段名
}

func main() {
    u := User{
        Name:    "小明",
        Address: Address{City: "北京", Street: "长安街"},
    }

    // 内嵌字段会被"提升":可以像自己的一样直接访问
    fmt.Println(u.City)         // 北京(等价于 u.Address.City)
    fmt.Println(u.Address.City) // 北京

    // 修改也一样
    u.Street = "建国路"
    fmt.Println(u.Address.Street)   // 建国路
}

组合的强大之处在于可以多层嵌套,内嵌结构体的方法也会被提升(下一篇讲方法时会看到,内嵌字段的方法能直接在外层结构体上调用)。这让 Go 在没有继承体系的情况下,依然能优雅地复用代码。除了结构体,基础类型(如 intstring)也能当匿名字段嵌入,可以用它给基础类型"起别名"并挂自定义方法。

四、结构体标签 tag

每个字段后面可以跟一个用反引号包裹的字符串,叫"标签"(tag)。标签本身不影响代码逻辑,但能被反射(reflect)读取,最经典的用途是控制 JSON 序列化:指定字段在 JSON 里的名字、是否在零值时省略、是否完全隐藏。

package main

import (
    "encoding/json"
    "fmt"
)

// 结构体标签:反引号包裹的元信息,常用于 JSON 序列化
type Product struct {
    Name  string `json:"name"`            // 序列化时键变成小写 name
    Price int    `json:"price,omitempty"`  // omitempty:零值则省略
    Code  string `json:"-"`                // 永远不输出
}

func main() {
    p := Product{Name: "手机", Price: 4999, Code: "X123"}
    b, _ := json.Marshal(p)
    fmt.Println(string(b))
    // 输出:{"name":"手机","price":4999}

    // 匿名结构体:临时用,不必先 type 定义
    point := struct {
        X, Y int
    }{X: 3, Y: 4}
    fmt.Println(point)   // {3 4}
}

json:"name" 把字段在输出 JSON 里改名为小写;omitempty 表示如果是零值就别出现在 JSON 里;json:"-" 则是"绝不输出",常用于密码、内部 ID 等敏感字段。除了 json,数据库 ORM(如 GORM)、表单校验(如 validator)也都用自己的 tag,套路一致。

五、匿名结构体

如果某个结构体只用一次,没必要预先 type 定义,可以就地写一个匿名结构体——直接 struct { X, Y int } 作为类型,后面紧跟花括号赋值。上面代码末尾的 point 就是这种用法,适合临时组装一点数据,比如解析一段只在此处用的 JSON。

六、值类型与零值

结构体是值类型,意味着:声明 var p Person 后即使没赋值,每个字段也已经有对应类型的零值——字符串是空串、数字是 0、指针是 nil。Go 没有"构造函数"的概念,但社区约定俗成写一个 NewPerson(...) 工厂函数返回初始化好的实例,既清楚又避免零值带来的意外。

小结

结构体本身只是"数据",下一篇 Go 方法 Method 给它装上"行为"。上一篇 Go Map 映射

✈️💬