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()
}
十四、最佳实践总结
- 用结构体封装业务模型(用户、订单、商品等)
- 优先使用指定字段名初始化
- 需要修改数据时用指针接收者
- 用嵌套结构体实现组合,替代继承
- 配合 tag 做 JSON/数据库映射
- 大写字段可导出,小写字段私有
结语
结构体是 Go 语言的灵魂,它简洁、强大、没有冗余设计。掌握了结构体,你就掌握了 Go 语言自定义类型、面向数据编程、业务建模的核心能力。
相比于其他语言的类,Go 结构体更简单、更直接、性能更好。建议你多动手写几个业务模型(比如订单、商品、文章),很快就能完全熟练使用!