404 Not Found

404 Not Found


nginx

结构体

结构体

想象你在填写一张个人信息登记表:姓名、年龄、电话、地址——这些信息彼此关联,共同描述一个人。在 Go 语言中,struct(结构体)就是这张"登记表":它将多个不同类型的字段组合在一起,形成一个自定义的复合类型。相比 map,struct 的字段在编译期就已确定,类型安全、语义清晰,是 Go 中组织数据的核心方式。


1. 核心概念

概念 说明
定义 type StructName struct { ... },字段名在前、类型在后
初始化 字面量 S{f1: v1, f2: v2}new(S) 返回指针、&S{}
字段访问 点号 s.Field,指针同样用点号 p.Field(无需 ->
值类型 struct 是值类型,赋值和传参会复制整个结构体
指针接收者 通过 &s 获取指针,修改指针指向的结构体会影响原值
匿名字段 只写类型不写字段名,类型名即字段名,实现"组合"
嵌套结构体 结构体字段本身也是结构体,支持多层嵌套
结构体标签 Tag 反引号包裹的元数据,常用于 JSON/数据库映射
⚠️ 注意:struct 是值类型,直接传参会复制一份。如需修改原值,应传指针 *S


2. 基本语法/用法

定义结构体

GO
// 定义一个 Person 结构体
type Person struct {
    Name string
    Age  int
    City string
}
💡 提示:Go 的结构体字段名如果以大写字母开头,则该字段是导出的(public);小写开头则是未导出的(private),仅包内可见。

初始化方式

GO
// 方式一:字段名: 值(推荐,可读性好,顺序无关)
p1 := Person{Name: "Alice", Age: 25, City: "北京"}

// 方式二:按顺序赋值(字段多时不推荐,容易出错)
p2 := Person{"Bob", 30, "上海"}

// 方式三:先声明后赋值
var p3 Person
p3.Name = "Charlie"
p3.Age = 28
p3.City = "广州"

// 方式四:new 创建(返回指针)
p4 := new(Person)  // *Person
p4.Name = "Diana"

// 方式五:取地址(返回指针)
p5 := &Person{Name: "Eve", Age: 22, City: "深圳"}
💡 提示:var p Person 声明的结构体,所有字段会被初始化为对应类型的零值(string → "",int → 0,bool → false)。

访问与修改字段

GO
p := Person{Name: "Alice", Age: 25}

// 读取字段
fmt.Println(p.Name)  // Alice

// 修改字段
p.Age = 26
fmt.Println(p.Age)   // 26
💡 提示:指针和值访问字段的语法完全一致——Go 会自动解引用,不需要 p->Name 这样的写法。

结构体指针

GO
p := &Person{Name: "Alice", Age: 25}

// Go 自动解引用,直接用点号访问
fmt.Println(p.Name)  // Alice

// 通过指针修改原值
p.Age = 30
💡 提示:Go 中没有 -> 运算符。无论 p 是值还是指针,都统一用 p.Field 访问。


3. 示例代码

示例 1:基础用法(难度⭐)

定义一个 Book 结构体,创建实例并打印信息。

GO
package main

import "fmt"

// Book 定义图书结构体
type Book struct {
    Title  string
    Author string
    Pages  int
    Price  float64
}

func main() {
    // 使用字面量创建
    book1 := Book{
        Title:  "Go语言编程",
        Author: "张三",
        Pages:  350,
        Price:  59.9,
    }

    // 打印结构体
    fmt.Println(book1)

    // 访问单个字段
    fmt.Printf("书名: %s\n", book1.Title)
    fmt.Printf("作者: %s\n", book1.Author)
    fmt.Printf("价格: %.1f 元\n", book1.Price)

    // 修改字段
    book1.Price = 49.9
    fmt.Printf("新价格: %.1f 元\n", book1.Price)

    // 按顺序创建(不推荐)
    book2 := Book{"Python入门", "李四", 280, 45.0}
    fmt.Println(book2)
}
▶ 试一试

输出

TEXT
{Go语言编程 张三 350 59.9}
书名: Go语言编程
作者: 张三
价格: 59.9 元
新价格: 49.9 元
{Python入门 李四 280 45}

示例 2:进阶用法(难度⭐⭐)

使用指针、嵌套结构体和匿名字段。

GO
package main

import "fmt"

// Address 地址结构体
type Address struct {
    City    string
    Street  string
    ZipCode string
}

// Employee 员工结构体(使用匿名字段 Address)
type Employee struct {
    Name    string
    Age     int
    Address        // 匿名字段:类型名即字段名
    Salary  float64
}

func main() {
    // ========== 嵌套结构体初始化 ==========
    emp := Employee{
        Name:   "Alice",
        Age:    30,
        Salary: 15000,
        Address: Address{
            City:    "北京",
            Street:  "中关村大街1号",
            ZipCode: "100080",
        },
    }

    fmt.Printf("员工: %s\n", emp.Name)
    fmt.Printf("城市: %s\n", emp.Address.City)  // 显式访问

    // ========== 匿名字段的"提升"访问 ==========
    // 匿名字段可以直接用类型名访问
    fmt.Printf("街道: %s\n", emp.Street)   // 等同于 emp.Address.Street
    fmt.Printf("邮编: %s\n", emp.ZipCode) // 等同于 emp.Address.ZipCode

    // ========== 指针操作 ==========
    empPtr := &emp

    // 通过指针修改(Go 自动解引用)
    empPtr.Age = 31
    empPtr.Salary = 18000
    empPtr.City = "上海" // 通过指针修改匿名字段

    fmt.Printf("\n修改后: %+v\n", *empPtr)

    // ========== 值类型 vs 指针类型 ==========
    fmt.Println("\n--- 值类型 vs 指针类型 ---")

    // 值类型:赋值是复制
    emp2 := emp
    emp2.Name = "Bob"
    fmt.Printf("emp.Name:  %s\n", emp.Name)  // 仍然是 Alice
    fmt.Printf("emp2.Name: %s\n", emp2.Name) // Bob

    // 指针类型:赋值是共享
    emp3 := empPtr
    emp3.Name = "Charlie"
    fmt.Printf("empPtr.Name: %s\n", empPtr.Name) // Charlie(被修改了)
}
▶ 试一试

输出

TEXT
员工: Alice
城市: 北京
街道: 中关村大街1号
邮编: 100080

修改后: {Name:Alice Age:31 Address:{City:上海 Street:中关村大街1号 ZipCode:100080} Salary:18000}

--- 值类型 vs 指针类型 ---
emp.Name:  Alice
emp2.Name: Bob
empPtr.Name: Charlie

示例 3:综合应用(难度⭐⭐⭐)

使用结构体标签(Tag)实现 JSON 序列化/反序列化,并构建一个学生管理系统。

GO
package main

import (
    "encoding/json"
    "fmt"
    "sort"
    "strings"
)

// Student 学生结构体(带 JSON 标签)
type Student struct {
    ID     int      `json:"id"`
    Name   string   `json:"name"`
    Age    int      `json:"age"`
    Score  float64  `json:"score"`
    Tags   []string `json:"tags,omitempty"` // omitempty: 空值时省略
    secret string   // 未导出字段,JSON 无法访问
}

// ClassRoom 班级(嵌套结构体)
type ClassRoom struct {
    ClassName string    `json:"class_name"`
    Students  []Student `json:"students"`
}

// AddStudent 添加学生
func (c *ClassRoom) AddStudent(s Student) {
    c.Students = append(c.Students, s)
}

// GetTopN 获取成绩前 N 名
func (c *ClassRoom) GetTopN(n int) []Student {
    // 复制一份,避免修改原数据
    sorted := make([]Student, len(c.Students))
    copy(sorted, c.Students)

    // 按成绩降序排序
    sort.Slice(sorted, func(i, j int) bool {
        return sorted[i].Score > sorted[j].Score
    })

    if n > len(sorted) {
        n = len(sorted)
    }
    return sorted[:n]
}

// Average 计算平均分
func (c *ClassRoom) Average() float64 {
    if len(c.Students) == 0 {
        return 0
    }
    total := 0.0
    for _, s := range c.Students {
        total += s.Score
    }
    return total / float64(len(c.Students))
}

// SearchByName 按名字搜索(模糊匹配)
func (c *ClassRoom) SearchByName(keyword string) []Student {
    var result []Student
    for _, s := range c.Students {
        if strings.Contains(s.Name, keyword) {
            result = append(result, s)
        }
    }
    return result
}

func main() {
    // 创建班级
    class := &ClassRoom{ClassName: "2024级一班"}

    // 添加学生
    students := []Student{
        {ID: 1, Name: "张三", Age: 18, Score: 92.5, Tags: []string{"数学好", "班长"}},
        {ID: 2, Name: "李四", Age: 19, Score: 88.0, Tags: []string{"体育好"}},
        {ID: 3, Name: "王五", Age: 18, Score: 95.5},
        {ID: 4, Name: "赵六", Age: 20, Score: 78.0, Tags: []string{"文艺特长"}},
        {ID: 5, Name: "张伟", Age: 19, Score: 91.0, Tags: []string{"数学好", "竞赛获奖"}},
    }

    for _, s := range students {
        class.AddStudent(s)
    }

    // ========== JSON 序列化 ==========
    fmt.Println("=== JSON 序列化 ===")
    jsonData, err := json.MarshalIndent(class, "", "  ")
    if err != nil {
        fmt.Println("序列化失败:", err)
    } else {
        fmt.Println(string(jsonData))
    }

    // ========== JSON 反序列化 ==========
    fmt.Println("\n=== JSON 反序列化 ===")
    jsonStr := `{"id":6,"name":"孙七","age":17,"score":97.0,"tags":["英语好"]}`
    var newStudent Student
    if err := json.Unmarshal([]byte(jsonStr), &newStudent); err != nil {
        fmt.Println("反序列化失败:", err)
    } else {
        fmt.Printf("新学生: %+v\n", newStudent)
    }

    // ========== 功能演示 ==========
    fmt.Println("\n=== 班级信息 ===")
    fmt.Printf("班级: %s,共 %d 人\n", class.ClassName, len(class.Students))
    fmt.Printf("平均分: %.1f\n", class.Average())

    fmt.Println("\n=== 成绩前3名 ===")
    top3 := class.GetTopN(3)
    for i, s := range top3 {
        fmt.Printf("  第%d名: %s (%.1f分)\n", i+1, s.Name, s.Score)
    }

    fmt.Println("\n=== 搜索 '张' ===")
    results := class.SearchByName("张")
    for _, s := range results {
        fmt.Printf("  %s (ID: %d, 分数: %.1f)\n", s.Name, s.ID, s.Score)
    }
}
▶ 试一试

输出

TEXT
=== JSON 序列化 ===
{
  "class_name": "2024级一班",
  "students": [
    {
      "id": 1,
      "name": "张三",
      "age": 18,
      "score": 92.5,
      "tags": [
        "数学好",
        "班长"
      ]
    },
    {
      "id": 2,
      "name": "李四",
      "age": 19,
      "score": 88
    },
    {
      "id": 3,
      "name": "王五",
      "age": 18,
      "score": 95.5
    },
    {
      "id": 4,
      "name": "赵六",
      "age": 20,
      "score": 78,
      "tags": [
        "文艺特长"
      ]
    },
    {
      "id": 5,
      "name": "张伟",
      "age": 19,
      "score": 91,
      "tags": [
        "数学好",
        "竞赛获奖"
      ]
    }
  ]
}

=== JSON 反序列化 ===
新学生: {ID:6 Name:孙七 Age:17 Score:97 Tags:[英语好]}

=== 班级信息 ===
班级: 2024级一班,共 5 人
平均分: 89.0

=== 成绩前3名 ===
  第1名: 王五 (95.5分)
  第2名: 张三 (92.5分)
  第3名: 张伟 (91.0分)

=== 搜索 '张' ===
  张三 (ID: 1, 分数: 92.5)
  张伟 (ID: 5, 分数: 91.0)

3. 常见应用场景

场景一:JSON 数据交互

在 Web 开发中,struct 是处理 JSON 请求/响应的标准方式。通过 Tag 控制 JSON 字段名。

GO
package main

import (
    "encoding/json"
    "fmt"
)

// APIResponse 统一 API 响应结构
type APIResponse struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

// UserDTO 用户数据传输对象
type UserDTO struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
    Password string `json:"-"` // "-" 表示 JSON 中忽略此字段
}

func main() {
    // 构造响应
    user := UserDTO{
        ID:       1,
        Username: "alice",
        Email:    "alice@example.com",
        Password: "secret123", // 不会被序列化
    }

    resp := APIResponse{
        Code:    200,
        Message: "success",
        Data:    user,
    }

    // 序列化
    jsonBytes, _ := json.MarshalIndent(resp, "", "  ")
    fmt.Println(string(jsonBytes))

    // 反序列化
    jsonStr := `{"code":404,"message":"用户不存在"}`
    var errResp APIResponse
    json.Unmarshal([]byte(jsonStr), &errResp)
    fmt.Printf("\n错误码: %d, 消息: %s\n", errResp.Code, errResp.Message)
}

输出

TEXT
{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "username": "alice",
    "email": "alice@example.com"
  }
}

错误码: 404, 消息: 用户不存在

场景二:面向对象模拟(组合优于继承)

Go 没有类和继承,通过结构体嵌套(组合)实现代码复用。

GO
package main

import "fmt"

// Logger 日志能力
type Logger struct {
    Prefix string
}

// Log 输出日志
func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.Prefix, msg)
}

// Cache 缓存能力
type Cache struct {
    data map[string]string
}

// Get 从缓存获取
func (c *Cache) Get(key string) (string, bool) {
    val, ok := c.data[key]
    return val, ok
}

// Set 设置缓存
func (c *Cache) Set(key, value string) {
    if c.data == nil {
        c.data = make(map[string]string)
    }
    c.data[key] = value
}

// UserService 通过组合复用 Logger 和 Cache 的能力
type UserService struct {
    Logger       // 嵌入 Logger
    Cache        // 嵌入 Cache
    Name string
}

func main() {
    svc := UserService{
        Logger: Logger{Prefix: "UserService"},
        Name:   "用户服务",
    }

    // 直接调用嵌入的方法
    svc.Log("服务启动")               // 来自 Logger
    svc.Set("user:1", "Alice")        // 来自 Cache
    svc.Log("缓存已初始化")

    if val, ok := svc.Get("user:1"); ok {
        svc.Log(fmt.Sprintf("找到用户: %s", val))
    }
}

输出

TEXT
[UserService] 服务启动
[UserService] 缓存已初始化
[UserService] 找到用户: Alice

❓ 常见问题

Q1:struct 是值类型还是引用类型?

struct 是值类型。赋值和传参会复制整个结构体。如果需要修改原值,应传指针:

GO
func updateAge(p *Person, age int) {
    p.Age = age  // 直接修改原值
}

p := Person{Name: "Alice", Age: 25}
updateAge(&p, 30)
fmt.Println(p.Age)  // 30

Q2:匿名字段有什么用?和嵌套结构体有什么区别?

匿名字段实现了"组合"(composition),内层类型的方法和字段会被"提升"到外层:

GO
type Address struct {
    City string
}

type Person struct {
    Name string
    Address        // 匿名字段
}

p := Person{Name: "Alice", Address: Address{City: "北京"}}
fmt.Println(p.City)         // 直接访问,等同于 p.Address.City
fmt.Println(p.Address.City) // 也可以显式访问

如果嵌套的结构体有同名字段,需要用完整路径访问来消除歧义。

Q3:结构体标签 Tag 是什么?怎么用?

Tag 是反引号包裹的元数据,常用于控制 JSON、数据库 ORM 等库的行为:

GO
type User struct {
    ID       int    `json:"id"              db:"user_id"`
    Name     string `json:"name"            db:"user_name"`
    Password string `json:"-"               db:"password"`    // JSON 忽略
    Email    string `json:"email,omitempty"  db:"email"`       // 空值时 JSON 忽略
}

常见 Tag:

Q4:结构体可以比较吗?

取决于字段类型。如果所有字段都是可比较类型(int、string、bool、数组等),结构体可以用 ==!= 比较。如果包含 slice、map、func 等不可比较字段,则不能直接比较:

GO
type Point struct { X, Y int }

p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2)  // true

// 包含 slice 的结构体不能用 ==
type Data struct { Items []int }
// d1 == d2  // 编译错误!

📖 小节


📝 作业

练习 1(⭐)

定义一个 Rectangle 结构体,包含 WidthHeight 两个字段。实现以下功能:

  1. 创建一个 Rectangle 实例并打印
  2. 编写 Area() 方法计算面积
  3. 编写 Perimeter() 方法计算周长
  4. 编写 IsSquare() 方法判断是否为正方形

练习 2(⭐⭐)

定义一个 BankAccount 结构体,包含 Owner(string)、Balance(float64)字段。实现以下功能:

  1. Deposit(amount) 存款
  2. Withdraw(amount) 取款(余额不足时提示)
  3. Transfer(other *BankAccount, amount) 转账
  4. 使用 JSON 标签序列化账户信息(隐藏余额,使用 json:"-"

练习 3(⭐⭐⭐)

实现一个商品库存管理系统

  1. 定义 Product 结构体(ID、Name、Price、Stock、Tags)
  2. 定义 Inventory 结构体(ShopName、Products 切片)
  3. 实现方法:AddProductRemoveProduct(id)FindByTag(tag)TotalValue()(总库存价值)
  4. 实现 JSON 序列化,要求:Price 保留两位小数,Stock 为 0 时省略
  5. 创建实例,添加商品,按标签搜索,计算总价值,最后 JSON 输出

下一课

👉 08-methods - 方法

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏