Go 语言从入门到进阶:6. 一文彻底吃透结构体(Struct)

Go 语言从入门到进阶:一文彻底吃透结构体(Struct)

在 Go 语言的世界里,没有面向对象编程中经典的继承,但结构体(Struct)凭借其灵活、高效的特性,成为了 Go 语言自定义数据类型封装数据的核心基石。

无论是写简单的业务模型,还是构建复杂的系统架构,结构体都是你必须掌握的核心技能。今天这篇文章,就带你从零到一,彻底搞懂 Go 结构体的定义、初始化、方法、嵌套等所有核心用法,看完就能上手实战!

一、什么是结构体?

简单来说:结构体是一组字段(属性)的集合,用来描述一个具体的事物

比如我们要描述一个「用户」,它有姓名、年龄、ID;描述一个「文章」,它有标题、内容、作者、发布时间。这些有多个属性的事物,都可以用结构体来定义。

它的作用:

  • 相关的数据打包在一起
  • 自定义属于自己的数据类型
  • 为数据绑定行为(方法)
  • 替代其他语言中的「类」

二、结构体的基本定义

结构体使用 type + struct 关键字定义,语法如下:

go 复制代码
// 定义格式
type 结构体名 struct {
    字段名 字段类型
    字段名 字段类型
    // ...
}

实战示例:定义一个用户结构体

go 复制代码
// 定义一个 User 结构体
type User struct {
    ID       int
    Username string
    Age      int
    Email    string
}

这里我们定义了一个User类型,它包含 4 个字段:ID、用户名、年龄、邮箱。

注意

  • 结构体名首字母大写:表示这个结构体可以被其他包访问(公共)
  • 字段名首字母大写:表示字段可以被外部访问(公共)
  • 小写则表示私有,只能在当前包内使用

三、结构体的初始化(创建实例)

定义好结构体后,我们需要创建结构体实例(类似其他语言 new 一个对象)。

Go 提供了 4 种常用初始化方式,适用于不同场景。

方式 1:按字段顺序初始化(不推荐)

go 复制代码
user := User{1, "zhangsan", 20, "zhangsan@qq.com"}

⚠️ 缺点:必须严格按字段顺序传值,可读性差,容易出错。


方式 2:指定字段名初始化(最常用、最推荐)

go 复制代码
user := User{
    ID:       1,
    Username: "zhangsan",
    Age:      20,
    Email:    "zhangsan@qq.com",
}

✅ 优点:清晰、安全、可缺省字段(未赋值的字段会用类型零值填充)。


方式 3:先声明,后赋值

go 复制代码
// 声明一个 User 类型变量,所有字段默认为零值
var user User

// 逐个赋值
user.ID = 1
user.Username = "zhangsan"
user.Age = 20
user.Email = "zhangsan@qq.com"

方式 4:使用 new 关键字(返回指针)

go 复制代码
user := new(User)
// 等价于 user := &User{}

user.ID = 1
user.Username = "zhangsan"

综合比较

cpp 复制代码
type Student struct {
    ID     int
    Name   string
    Score  float64
}

func main() {
    // 方式1:直接声明
    var s1 Student
    s1.ID = 1001
    s1.Name = "Alice"
    
    // 方式2:字面量初始化(按字段顺序)
    s2 := Student{1002, "Bob", 88.5}
    
    // 方式3:字段名初始化(推荐!顺序无关)
    s3 := Student{
        ID:    1003,
        Name:  "Charlie",
        Score: 92.0,
    }
    
    // 方式4:new函数(返回指针)
    s4 := new(Student)
    s4.ID = 1004
    s4.Name = "David"
    
    // 方式5:取地址初始化
    s5 := &Student{ID: 1005, Name: "Eve", Score: 95.5}
    
    fmt.Println(s1, s2, s3, s4, s5)
}

四、访问与修改结构体字段

访问结构体字段使用 . 符号,非常直观。

go 复制代码
package main

import "fmt"

type User struct {
    ID       int
    Username string
    Age      int
}

func main() {
    // 初始化
    u := User{
        ID:       1,
        Username: "张三",
        Age:      20,
    }

    // 访问字段
    fmt.Println("用户ID:", u.ID)
    fmt.Println("用户名:", u.Username)

    // 修改字段
    u.Age = 21
    fmt.Println("修改后年龄:", u.Age)
}

输出

复制代码
用户ID: 1
用户名: 张三
修改后年龄: 21

五、结构体方法(给结构体绑定行为)

这是结构体最强大的地方:可以给结构体定义方法,让数据拥有行为。

方法和函数很像,但必须指定接收者(receiver),表示这个方法属于哪个结构体。

语法

go 复制代码
func (接收者变量 接收者类型) 方法名(参数) 返回值 {
    // 方法体
}

实战:给 User 添加一个方法

go 复制代码
// 给 User 定义一个方法:输出用户信息
func (u User) ShowInfo() {
    fmt.Printf("用户:%s,年龄:%d\n", u.Username, u.Age)
}

// 调用
u.ShowInfo()

指针接收者 vs 值接收者(重点!)

这是 Go 面试和实战中最常考、最容易错的点。

1. 值接收者(拷贝副本,不修改原数据)
go 复制代码
func (u User) ChangeName(newName string) {
    u.Username = newName // 只修改副本
}
2. 指针接收者(直接操作原数据,可修改原结构体)
go 复制代码
func (u *User) ChangeName(newName string) {
    u.Username = newName // 直接修改原结构体
}

总结

  • 需要修改 结构体内部数据 → 用指针接收者
  • 不需要修改,只是读取数据 → 用值接收者
  • 结构体很大 ,拷贝耗性能 → 用指针接收者(避免拷贝开销)
  • 保持方法调用一致性(如果某个方法用了指针,最好都用)

综合实现比较

cpp 复制代码
type Counter struct {
    count int
}

// 值接收者:不会修改原值
func (c Counter) Increment() {
    c.count++  // 修改的是副本
}

// 指针接收者:会修改原值
func (c *Counter) IncrementReal() {
    c.count++  // 修改原值
}

// 获取值(值接收者也可以,但通常用值接收者获取数据)
func (c Counter) Value() int {
    return c.count
}

func main() {
    c := Counter{count: 0}
    
    c.Increment()
    fmt.Println(c.Value())  // 0,没变!
    
    c.IncrementReal()
    fmt.Println(c.Value())  // 1,变了!
}

type BigStruct struct {
    Data [1024]int
}

// 大结构体用指针接收者
func (b *BigStruct) Process() {
    // 处理数据
}

// 小结构体或只读操作可以用值接收者
type Point struct {
    X, Y int
}

func (p Point) Distance() float64 {
    return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}

六、结构体嵌套(模拟继承)

Go 没有继承,但可以通过嵌套结构体 实现组合,这也是 Go 官方推荐的方式。

示例:嵌套结构体

go 复制代码
// 地址结构体
type Address struct {
    Province string
    City     string
}

// 用户结构体嵌套 Address
type User struct {
    Username string
    Age      int
    Addr     Address // 嵌套结构体
}

使用

go 复制代码
u := User{
    Username: "张三",
    Age:      20,
    Addr: Address{
        Province: "广东",
        City:     "深圳",
    },
}

fmt.Println(u.Addr.City) // 深圳

匿名嵌套(更像继承)

go 复制代码
type User struct {
    Username string
    Age      int
    Address  // 匿名嵌套
}

可以直接访问嵌套字段:

go 复制代码
fmt.Println(u.City) // 直接访问,不需要 u.Addr.City

多重嵌入(小心实现!)

cpp 复制代码
type Reader struct {}
func (r Reader) Read() string { return "reading..." }

type Writer struct {}
func (w Writer) Write() string { return "writing..." }

// 多重嵌入
type ReadWriter struct {
    Reader
    Writer
}

func main() {
    rw := ReadWriter{}
    fmt.Println(rw.Read())   // reading...
    fmt.Println(rw.Write())  // writing...
}

综合实现

cpp 复制代码
// 基础结构体
type Animal struct {
    Name string
    Age  int
}

func (a Animal) Speak() string {
    return "some sound"
}

// 嵌入Animal
type Dog struct {
    Animal      // 匿名嵌入
    Breed string // 狗特有的字段
}

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

type Cat struct {
    Animal
    Color string
}

func main() {
    dog := Dog{
        Animal: Animal{Name: "Buddy", Age: 3},
        Breed:  "Golden Retriever",
    }
    
    // 可以直接访问嵌入字段
    fmt.Println(dog.Name)  // Buddy
    fmt.Println(dog.Age)   // 3
    fmt.Println(dog.Speak()) // Woof!
    
    cat := Cat{
        Animal: Animal{Name: "Whiskers", Age: 2},
        Color:  "Orange",
    }
    fmt.Println(cat.Name)  // Whiskers
    fmt.Println(cat.Speak()) // some sound(未重写)
}

七、结构体与 JSON 序列化(实战必备)

实际开发中,结构体最常用的场景之一:和 JSON 互相转换

使用 encoding/json 包,配合结构体标签(Tag)

示例

go 复制代码
type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Age      int    `json:"age"`
}

// 结构体转 JSON
u := User{1, "张三", 20}
data, _ := json.Marshal(u)
fmt.Println(string(data))

输出

json 复制代码
{"id":1,"username":"张三","age":20}

标签作用:指定 JSON 中的键名,实现格式映射。

综合实现

cpp 复制代码
import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID       int    `json:"id"`                // JSON字段名为id
    Username string `json:"username"`          // JSON字段名为username
    Password string `json:"-"`                 // 忽略该字段
    Email    string `json:"email,omitempty"`   // 为空时不输出
    CreatedAt string `json:"created_at,omitempty"`
}

func main() {
    user := User{
        ID:       1,
        Username: "alice",
        Password: "secret123",
        Email:    "",
        CreatedAt: "2024-01-01",
    }
    
    // 序列化为JSON
    jsonData, _ := json.Marshal(user)
    fmt.Println(string(jsonData))
    // 输出: {"id":1,"username":"alice","created_at":"2024-01-01"}
    
    // JSON反序列化
    jsonStr := `{"id":2,"username":"bob","email":"bob@example.com"}`
    var newUser User
    json.Unmarshal([]byte(jsonStr), &newUser)
    fmt.Printf("%+v\n", newUser)
    // 输出: {ID:2 Username:bob Password: Email:bob@example.com CreatedAt:}
}

八、结构体的零值

如果只声明不初始化,结构体不会是 nil,而是所有字段为对应类型零值

go 复制代码
var u User
fmt.Println(u.Username) // 空字符串
fmt.Println(u.Age)      // 0

九、结构体比较

  • 所有字段都可比较 的结构体,结构体本身可以用 == 比较
  • 包含切片、map 等不可比较字段的结构体,不能直接比较
go 复制代码
u1 := User{ID: 1, Username: "张三"}
u2 := User{ID: 1, Username: "张三"}

fmt.Println(u1 == u2) // true

十、构造函数模式

go 复制代码
package main

import (
    "errors"
    "fmt"
)

type Config struct {
    Host     string
    Port     int
    Username string
    Password string
}

// 构造函数
func NewConfig(host string, port int) *Config {
    return &Config{
        Host: host,
        Port: port,
    }
}

// 带验证的构造函数
func NewConfigWithValidation(host string, port int) (*Config, error) {
    if host == "" {
        return nil, errors.New("host不能为空")
    }
    if port < 1 || port > 65535 {
        return nil, errors.New("端口范围1-65535")
    }
    return &Config{
        Host: host,
        Port: port,
    }, nil
}

// Builder模式(配置项较多时使用)
type ConfigBuilder struct {
    config Config
}

func NewConfigBuilder() *ConfigBuilder {
    return &ConfigBuilder{
        config: Config{
            Host: "localhost", // 默认值
            Port: 8080,         // 默认值
        },
    }
}

func (b *ConfigBuilder) SetHost(host string) *ConfigBuilder {
    b.config.Host = host
    return b
}

func (b *ConfigBuilder) SetPort(port int) *ConfigBuilder {
    b.config.Port = port
    return b
}

func (b *ConfigBuilder) SetAuth(username, password string) *ConfigBuilder {
    b.config.Username = username
    b.config.Password = password
    return b
}

func (b *ConfigBuilder) Build() Config {
    return b.config
}

func main() {
    // 简单构造函数
    config1 := NewConfig("localhost", 3000)
    fmt.Printf("%+v\n", config1)
    
    // Builder模式
    config2 := NewConfigBuilder().
        SetHost("192.168.1.1").
        SetPort(5432).
        SetAuth("admin", "pass123").
        Build()
    fmt.Printf("%+v\n", config2)
}

十一、杂项

1. 空结构体 - 内存优化

go 复制代码
// 空结构体不占用内存
type Empty struct{}

// 常用作set的value
set := make(map[string]struct{})
set["item"] = struct{}{}

// 或者用作信号通道
done := make(chan struct{})

2. 性能优化:字段顺序

go 复制代码
// 不好的顺序(可能造成内存对齐浪费)
type BadOrder struct {
    flag  bool    // 1 byte
    score float64 // 8 bytes(实际需要8字节对齐,会浪费7字节)
    age   int32   // 4 bytes
}
// 内存占用: 24 bytes

// 好的顺序(按大小排序)
type GoodOrder struct {
    score float64 // 8 bytes
    age   int32   // 4 bytes
    flag  bool    // 1 byte
}
// 内存占用: 16 bytes

3. 复制与深拷贝

go 复制代码
type Data struct {
    Values []int
}

// 浅拷贝(共享底层数组)
func (d Data) ShallowCopy() Data {
    return d
}

// 深拷贝
func (d Data) DeepCopy() Data {
    newValues := make([]int, len(d.Values))
    copy(newValues, d.Values)
    return Data{Values: newValues}
}

十二、常见错误与避坑指南

go 复制代码
// 错误1:使用nil指针
var user *User
user.Name = "Alice"  // panic!nil指针解引用

// 正确做法
user := &User{Name: "Alice"}

// 错误2:map未初始化
type School struct {
    Students map[string]Student
}
s := School{}
s.Students["001"] = Student{}  // panic!map为nil

// 正确做法
s := School{
    Students: make(map[string]Student),
}

// 错误3:方法接收者混淆
type Timer struct {
    duration time.Duration
}

func (t Timer) Duration() time.Duration {  // 值接收者
    return t.duration
}

func (t Timer) SetDuration(d time.Duration) {  // 错误!不会修改原值
    t.duration = d
}

// 正确做法
func (t *Timer) SetDuration(d time.Duration) {
    t.duration = d
}

十三、实战案例:简单的图书管理系统

go 复制代码
package main

import (
    "errors"
    "fmt"
    "time"
)

// 图书结构体
type Book struct {
    ID        string
    Title     string
    Author    string
    ISBN      string
    Status    BookStatus
    CreatedAt time.Time
}

type BookStatus string

const (
    StatusAvailable BookStatus = "available"
    StatusBorrowed  BookStatus = "borrowed"
    StatusReserved  BookStatus = "reserved"
)

// 用户结构体
type User struct {
    ID       string
    Name     string
    Email    string
    Borrowed []string // 借阅的图书ID列表
}

// 图书馆结构体
type Library struct {
    Name  string
    Books map[string]*Book
    Users map[string]*User
}

// 创建新图书馆
func NewLibrary(name string) *Library {
    return &Library{
        Name:  name,
        Books: make(map[string]*Book),
        Users: make(map[string]*User),
    }
}

// 添加图书
func (l *Library) AddBook(book *Book) error {
    if _, exists := l.Books[book.ID]; exists {
        return errors.New("图书ID已存在")
    }
    l.Books[book.ID] = book
    return nil
}

// 借书
func (l *Library) BorrowBook(userID, bookID string) error {
    user, exists := l.Users[userID]
    if !exists {
        return errors.New("用户不存在")
    }
    
    book, exists := l.Books[bookID]
    if !exists {
        return errors.New("图书不存在")
    }
    
    if book.Status != StatusAvailable {
        return fmt.Errorf("图书不可借,当前状态: %s", book.Status)
    }
    
    book.Status = StatusBorrowed
    user.Borrowed = append(user.Borrowed, bookID)
    fmt.Printf("%s 借阅了《%s》\n", user.Name, book.Title)
    return nil
}

// 还书
func (l *Library) ReturnBook(userID, bookID string) error {
    user, exists := l.Users[userID]
    if !exists {
        return errors.New("用户不存在")
    }
    
    // 检查用户是否借了这本书
    found := false
    for i, id := range user.Borrowed {
        if id == bookID {
            user.Borrowed = append(user.Borrowed[:i], user.Borrowed[i+1:]...)
            found = true
            break
        }
    }
    
    if !found {
        return errors.New("用户未借阅此书")
    }
    
    book := l.Books[bookID]
    book.Status = StatusAvailable
    fmt.Printf("%s 归还了《%s》\n", user.Name, book.Title)
    return nil
}

// 显示所有图书
func (l *Library) ShowBooks() {
    fmt.Printf("\n=== %s 藏书列表 ===\n", l.Name)
    for _, book := range l.Books {
        status := string(book.Status)
        fmt.Printf("《%s》 - %s (ISBN: %s) [%s]\n", 
            book.Title, book.Author, book.ISBN, status)
    }
}

func main() {
    // 创建图书馆
    lib := NewLibrary("智慧图书馆")
    
    // 添加用户
    lib.Users["U001"] = &User{
        ID:    "U001",
        Name:  "张三",
        Email: "zhangsan@example.com",
    }
    
    // 添加图书
    lib.AddBook(&Book{
        ID:        "B001",
        Title:     "Go语言实战",
        Author:    "William Kennedy",
        ISBN:      "978-7-121-12345-6",
        Status:    StatusAvailable,
        CreatedAt: time.Now(),
    })
    
    lib.AddBook(&Book{
        ID:        "B002",
        Title:     "算法导论",
        Author:    "Thomas H. Cormen",
        ISBN:      "978-7-111-12345-7",
        Status:    StatusAvailable,
        CreatedAt: time.Now(),
    })
    
    // 演示借还书
    lib.ShowBooks()
    lib.BorrowBook("U001", "B001")
    lib.ShowBooks()
    lib.ReturnBook("U001", "B001")
    lib.ShowBooks()
}

十四、最佳实践总结

  1. 用结构体封装业务模型(用户、订单、商品等)
  2. 优先使用指定字段名初始化
  3. 需要修改数据时用指针接收者
  4. 用嵌套结构体实现组合,替代继承
  5. 配合 tag 做 JSON/数据库映射
  6. 大写字段可导出,小写字段私有

结语

结构体是 Go 语言的灵魂,它简洁、强大、没有冗余设计。掌握了结构体,你就掌握了 Go 语言自定义类型、面向数据编程、业务建模的核心能力。

相比于其他语言的类,Go 结构体更简单、更直接、性能更好。建议你多动手写几个业务模型(比如订单、商品、文章),很快就能完全熟练使用!

相关推荐
ch.ju1 小时前
Java Programming Chapter 4——Private attribute
java·开发语言
CTA终结者1 小时前
期货量化环境装不上怎么办:天勤 TqSdk 安装与 Python 版本排查
开发语言·python
影寂ldy1 小时前
C# 多态与函数重载(静态多态)
开发语言·c#
SilentSamsara1 小时前
Python 与 Docker:多阶段构建、最小镜像与健康检查
运维·开发语言·python·docker·中间件·容器
变量未定义~1 小时前
快速幂、费马小定理、约数的个数、欧拉函数模板、矩阵快速幂
开发语言
hyunbar2 小时前
NOT IN 的 NULL 陷阱:一次 UNION 数据“神秘消失“
开发语言·sql
C+++Python2 小时前
如何在 Java 中使用 BIO、NIO 和 AIO?
java·开发语言·nio
189228048612 小时前
NV022固态MT29F16T08GWLCEM5-QBES:C
c语言·开发语言