Golang - 4.结构体、方法与 JSON 序列化详解

Go 结构体、方法与 JSON 序列化详解


一、结构体(Struct)

1.1 定义结构体

结构体是 Go 中组织数据的核心方式,将多个字段组合为一个命名整体:

type Person struct {
    Name string
    Age  int
}

1.2 创建结构体实例

// 方式一:按字段名初始化(推荐)
p1 := Person{Name: "Alice", Age: 30}

// 方式二:按顺序初始化(不推荐,字段增删易出错)
p2 := Person{"Bob", 25}

// 方式三:零值初始化
var p3 Person // Name: "", Age: 0

// 方式四:使用 new(返回指针,零值)
p4 := new(Person) // *Person, Name: "", Age: 0

// 方式五:取地址初始化(推荐,可修改)
p5 := &Person{Name: "Charlie", Age: 28}

1.3 访问与修改字段

p := Person{Name: "Alice", Age: 30}

// 访问
fmt.Println(p.Name) // Alice
fmt.Println(p.Age)  // 30

// 修改
p.Age = 31
fmt.Println(p.Age) // 31

1.4 结构体指针访问字段

Go 自动解引用,指针访问字段无需 (*p).Field

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

fmt.Println(p.Name) // Alice — 自动解引用,等价于 (*p).Name
p.Age = 31          // 自动解引用赋值

1.5 结构体是值类型

赋值和传参会产生独立副本,互不影响:

p1 := Person{Name: "Alice", Age: 30}
p2 := p1        // 完整拷贝
p2.Name = "Bob"

fmt.Println(p1.Name) // Alice — 原值不变
fmt.Println(p2.Name) // Bob

1.6 匿名字段(嵌入/组合)

结构体可以嵌入其他结构体,实现类似继承的组合

type Address struct {
    City    string
    Country string
}

type Employee struct {
    Person        // 匿名字段,嵌入 Person
    Address       // 匿名字段,嵌入 Address
    Company string
}

func main() {
    e := Employee{
        Person:  Person{Name: "Alice", Age: 30},
        Address: Address{City: "Beijing", Country: "China"},
        Company: "Acme",
    }

    // 直接访问嵌入字段的成员(字段提升)
    fmt.Println(e.Name)    // Alice — 等价于 e.Person.Name
    fmt.Println(e.City)    // Beijing — 等价于 e.Address.City
    fmt.Println(e.Company) // Acme
}

1.7 字段标签(Tag)

字段标签是附加在字段上的元信息,常用于 JSON、数据库映射等:

type User struct {
    Name string `json:"name" db:"user_name"`
    Age  int    `json:"age" db:"user_age"`
}

通过反射读取标签:

import "reflect"

t := reflect.TypeOf(User{})
field, _ := t.FieldByName("Name")
fmt.Println(field.Tag.Get("json")) // name
fmt.Println(field.Tag.Get("db"))   // user_name

1.8 结构体比较

如果所有字段都是可比较的,结构体可以用 == 比较:

p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Alice", Age: 30}
p3 := Person{Name: "Bob", Age: 25}

fmt.Println(p1 == p2) // true
fmt.Println(p1 == p3) // false

包含切片、Map 的结构体不可比较,编译会报错。


二、方法(Method)

2.1 方法定义

方法就是带接收者的函数,绑定到特定类型上:

// 语法:func (接收者) 方法名(参数) 返回值 { ... }

type Circle struct {
    Radius float64
}

// 值接收者
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

func main() {
    c := Circle{Radius: 5}
    fmt.Println(c.Area()) // 78.53975
}

2.2 值接收者 vs 指针接收者

特性 值接收者 func (c Circle) 指针接收者 func (c *Circle)
操作的是 副本 原始数据
能否修改原始数据 ❌ 不能 ✅ 能
调用方式 c.Method()(&c).Method() c.Method()(&c).Method()
适用场景 只读、小型结构体 需要修改、大型结构体
type Counter struct {
    count int
}

// 值接收者:无法修改原始数据
func (c Counter) Value() int {
    return c.count
}

// 指针接收者:可以修改原始数据
func (c *Counter) Increment() {
    c.count++
}

func main() {
    c := Counter{count: 0}
    c.Increment()
    c.Increment()
    fmt.Println(c.Value()) // 2
}

2.3 Go 自动转换接收者

Go 会自动在值和指针之间转换接收者:

c := Counter{count: 0}
p := &c

// 值调用指针方法 — Go 自动取地址
c.Increment() // 等价于 (&c).Increment()

// 指针调用值方法 — Go 自动解引用
p.Value()     // 等价于 (*p).Value()

2.4 选择原则

何时使用指针接收者:
- 方法需要修改接收者
- 结构体较大,避免拷贝开销
- 保持一致性:如果一个方法用指针接收者,其他方法也应如此

何时使用值接收者:
- 基本类型、小型结构体
- 不可变对象(如 time.Time

2.5 任何类型都可以有方法

不仅结构体,任何自定义类型都可以定义方法:

type MyString string

func (s MyString) Shout() string {
    return string(s) + "!!!"
}

type IntSlice []int

func (s IntSlice) Sum() int {
    total := 0
    for _, v := range s {
        total += v
    }
    return total
}

func main() {
    s := MyString("hello")
    fmt.Println(s.Shout()) // hello!!!

    nums := IntSlice{1, 2, 3, 4}
    fmt.Println(nums.Sum()) // 10
}

注意:不能给其他包的类型定义方法(包括内置类型),但可以通过自定义类型间接实现。

2.6 方法值与方法表达式

type Rect struct {
    Width, Height float64
}

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

func main() {
    r := Rect{Width: 3, Height: 4}

    // 方法调用
    fmt.Println(r.Area()) // 12

    // 方法值:绑定接收者的函数
    area := r.Area
    fmt.Println(area()) // 12

    // 方法表达式:需要显式传接收者
    area2 := Rect.Area
    fmt.Println(area2(r)) // 12
}

2.7 嵌入结构体的方法继承

嵌入的结构体方法会被提升到外层结构体:

type Animal struct {
    Name string
}

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

type Dog struct {
    Animal
    Breed string
}

func main() {
    d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
    fmt.Println(d.Speak()) // Rex speaks — 方法提升
    fmt.Println(d.Animal.Speak()) // Rex speaks — 也可显式调用
}

2.8 方法重写

外层结构体可以定义同名方法来覆盖嵌入方法:

func (d Dog) Speak() string {
    return d.Name + " barks"
}

func main() {
    d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
    fmt.Println(d.Speak())        // Rex barks — Dog 自己的方法
    fmt.Println(d.Animal.Speak()) // Rex speaks — 仍可访问嵌入方法
}

三、JSON 序列化与反序列化

3.1 基本序列化(结构体 → JSON)

使用 json.Marshal

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    p := Person{Name: "Alice", Age: 30}

    data, err := json.Marshal(p)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data)) // {"name":"Alice","age":30}
}

3.2 基本反序列化(JSON → 结构体)

使用 json.Unmarshal

jsonStr := `{"name":"Bob","age":25}`

var p Person
err := json.Unmarshal([]byte(jsonStr), &p)
if err != nil {
    panic(err)
}
fmt.Printf("%+v\n", p) // {Name:Bob Age:25}

注意:必须传指针给 Unmarshal,否则无法修改结构体。

3.3 JSON Tag 详解

Tag 控制字段与 JSON 键名的映射关系:

type User struct {
    // 常用格式:指定 JSON 键名
    Name string `json:"name"`

    // 忽略字段(序列化和反序列化都跳过)
    Secret string `json:"-"`

    // 序列化时字段为零值则省略
    Email  string `json:"email,omitempty"`
    Phone  string `json:"phone,omitempty"`

    // 忽略仅序列化时的输出,反序列化仍可赋值
    Token string `json:",omitempty"`

    // 键名含逗号等特殊字符(少见)
    Field string `json:"field,omitempty"`
}

Tag 选项汇总

Tag 说明
json:"name" 映射为 JSON 键 name
json:"-" 完全忽略该字段
json:"name,omitempty" 零值时省略该字段
json:",omitempty" 使用字段名(小写开头),零值时省略
json:",inline" 内联嵌入结构体的字段(Go 1.16+)

3.4 omitempty 详解

omitempty 在字段值为零值时跳过该字段:

type Response struct {
    Code    int    `json:"code"`
    Message string `json:"message,omitempty"`
    Data    any    `json:"data,omitempty"`
}

func main() {
    r1 := Response{Code: 200, Message: "OK", Data: []int{1, 2, 3}}
    d1, _ := json.Marshal(r1)
    fmt.Println(string(d1)) // {"code":200,"message":"OK","data":[1,2,3]}

    r2 := Response{Code: 404}
    d2, _ := json.Marshal(r2)
    fmt.Println(string(d2)) // {"code":404} — message 和 data 为零值被省略
}

各类型零值

类型 零值 omitempty 是否省略
int, float 0 ✅ 省略
string "" ✅ 省略
bool false ✅ 省略
pointer nil ✅ 省略
slice nil ✅ 省略
map nil ✅ 省略
interface nil ✅ 省略
struct 零值结构体 不省略

陷阱:结构体的零值不会被 omitempty 省略。如果需要省略,使用指针

type Order struct {
    Item  string  `json:"item"`
    Price *float64 `json:"price,omitempty"` // 指针,nil 时省略
}

func main() {
    o := Order{Item: "Apple"}
    d, _ := json.Marshal(o)
    fmt.Println(string(d)) // {"item":"Apple"} — price 被省略

    price := 3.99
    o2 := Order{Item: "Apple", Price: &price}
    d2, _ := json.Marshal(o2)
    fmt.Println(string(d2)) // {"item":"Apple","price":3.99}
}

3.5 美化输出(MarshalIndent)

p := Person{Name: "Alice", Age: 30}

data, _ := json.MarshalIndent(p, "", "  ")
fmt.Println(string(data))

输出:

{
  "name": "Alice",
  "age": 30
}

3.6 嵌套结构体

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"`
}

func main() {
    e := Employee{
        Name: "Alice",
        Age:  30,
        Address: Address{City: "Beijing", Country: "China"},
    }

    data, _ := json.MarshalIndent(e, "", "  ")
    fmt.Println(string(data))
}

输出:

{
  "name": "Alice",
  "age": 30,
  "address": {
    "city": "Beijing",
    "country": "China"
  }
}

3.7 匿名嵌入与 JSON

嵌入结构体默认以类型名为键:

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

type Employee struct {
    Name string  `json:"name"`
    Address       // 匿名嵌入
}

输出:{"name":"Alice","Address":{"city":"Beijing","country":"China"}}

若要内联(提升字段到外层),使用 ,inline

type Employee struct {
    Name    string  `json:"name"`
    Address Address `json:",inline"`
}

输出:{"name":"Alice","city":"Beijing","country":"China"}

3.8 处理未知字段

反序列化时,JSON 中的多余字段默认被忽略。如需捕获:

type Flexible struct {
    Name string                 `json:"name"`
    Ext  map[string]interface{} `json:"-"` // 用 - 忽略,手动处理
}

更常见的做法是直接用 map[string]any

var data map[string]any
json.Unmarshal([]byte(`{"name":"Alice","extra":"value"}`), &data)
fmt.Println(data) // map[name:Alice extra:value]

3.9 自定义序列化(MarshalJSON / UnmarshalJSON)

实现 json.Marshalerjson.Unmarshaler 接口:

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

type Event struct {
    Name      string    `json:"name"`
    Timestamp time.Time `json:"timestamp"`
}

// 自定义序列化:时间格式化为 Unix 时间戳
func (e Event) MarshalJSON() ([]byte, error) {
    type Alias Event // 避免递归调用
    return json.Marshal(&struct {
        Timestamp int64 `json:"timestamp"`
        *Alias
    }{
        Timestamp: e.Timestamp.Unix(),
        Alias:     (*Alias)(&e),
    })
}

// 自定义反序列化
func (e *Event) UnmarshalJSON(data []byte) error {
    type Alias Event
    aux := &struct {
        Timestamp int64 `json:"timestamp"`
        *Alias
    }{
        Alias: (*Alias)(e),
    }
    if err := json.Unmarshal(data, aux); err != nil {
        return err
    }
    e.Timestamp = time.Unix(aux.Timestamp, 0)
    return nil
}

3.10 处理动态 JSON

当 JSON 结构不确定时,使用 interface{}any

// 方式一:map[string]any
var result map[string]any
json.Unmarshal([]byte(`{"name":"Alice","scores":[90,85,92]}`), &result)
fmt.Println(result["name"])   // Alice
fmt.Println(result["scores"]) // [90 85 92]

// 方式二:json.RawMessage 延迟解析
type Envelope struct {
    Type string          `json:"type"`
    Data json.RawMessage `json:"data"` // 原始 JSON 字节
}

raw := `{"type":"user","data":{"name":"Alice","age":30}}`
var env Envelope
json.Unmarshal([]byte(raw), &env)

if env.Type == "user" {
    var u Person
    json.Unmarshal(env.Data, &u)
    fmt.Printf("%+v\n", u)
}

3.11 流式读写(Encoder / Decoder)

适用于 HTTP 请求/响应、文件读写等场景:

import (
    "encoding/json"
    "os"
)

// 写入文件
func writeJSON(filename string, v any) error {
    f, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    return json.NewEncoder(f).Encode(v)
}

// 读取文件
func readJSON(filename string, v any) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    return json.NewDecoder(f).Decode(v)
}

HTTP 场景:

import (
    "encoding/json"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    // 请求解码
    var req Person
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // 响应编码
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(req)
}

四、综合实战

4.1 完整示例:REST API 数据模型

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type Gender string

const (
    Male   Gender = "male"
    Female Gender = "female"
)

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

type User struct {
    ID        int      `json:"id"`
    Name      string   `json:"name"`
    Email     string   `json:"email,omitempty"`
    Gender    Gender   `json:"gender"`
    Address   Address  `json:"address,omitempty"`
    CreatedAt int64    `json:"created_at"`
    UpdatedAt *int64   `json:"updated_at,omitempty"` // 指针,nil 时省略
    Tags      []string `json:"tags,omitempty"`
}

// 方法:创建新用户
func NewUser(name string, gender Gender) *User {
    return &User{
        Name:      name,
        Gender:    gender,
        CreatedAt: time.Now().Unix(),
        Tags:      []string{},
    }
}

// 方法:设置邮箱
func (u *User) SetEmail(email string) {
    u.Email = email
}

// 方法:添加标签
func (u *User) AddTag(tag string) {
    u.Tags = append(u.Tags, tag)
}

// 方法:标记更新
func (u *User) Touch() {
    now := time.Now().Unix()
    u.UpdatedAt = &now
}

func main() {
    // 创建用户
    user := NewUser("Alice", Female)
    user.SetEmail("alice@example.com")
    user.AddTag("vip")
    user.AddTag("active")
    user.Address = Address{City: "Beijing", Country: "China"}

    // 序列化
    data, _ := json.MarshalIndent(user, "", "  ")
    fmt.Println("=== 序列化结果 ===")
    fmt.Println(string(data))

    // 反序列化
    jsonStr := `{
        "id": 1,
        "name": "Bob",
        "gender": "male",
        "address": {"city": "Shanghai", "country": "China"},
        "created_at": 1700000000
    }`

    var user2 User
    json.Unmarshal([]byte(jsonStr), &user2)
    fmt.Printf("\n=== 反序列化结果 ===\n%+v\n", user2)
}

4.2 JSON 序列化常见问题速查

问题 原因 解决方案
字段未序列化 字段名小写开头(未导出) 改为大写开头 + json tag
零值字段不想输出 Age: 0 也会输出 omitempty
结构体零值未被省略 omitempty 对结构体无效 改为指针 *Struct
时间格式不理想 time.Time 默认 RFC3339 自定义 MarshalJSON
数字类型变成 float64 map[string]any 反序列化 使用 json.Number 或定义结构体
循环引用导致栈溢出 结构体互相引用 用指针 + omitempty 或自定义序列化
JSON 键名与字段名不同 Go 风格大写,JSON 风格小写 使用 json:"key_name" tag

五、要点速记

结构体:值类型,赋值拷贝,修改需用指针
方法:  值接收者读,指针接收者改,保持一致性
JSON:  大写导出 + json tag,omitempty 省零值
        结构体零值不省 → 用指针
        动态 JSON → map[string]any / json.RawMessage
        流式处理 → json.Encoder / json.Decoder

AI 助手 - deepseek-v4-flash

你好!有什么可以帮你的吗?

可以问我:推荐文章、搜索主题、了解博客内容

AI 生成内容仅供参考

未播放
0:00 / 0:00