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.Marshaler 和 json.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