Skip to content

第29章:自定义数据类型 ​

本章目标 ​

学完本章后,你将能够:

  1. 理解 Scanner 和 Valuer 接口
  2. 实现自定义 JSON、枚举、加密类型
  3. 使用 serializer 标签简化序列化
  4. 根据需求选择 Scanner/Valuer 还是 Serializer

前置知识 ​

  • 需要先学习:第06-07章(模型标签)
  • 需要了解:Go 接口基本概念

29.1 Scanner 和 Valuer 接口 ​

Go 标准库 database/sql 定义了两个接口:

go
// Scanner:从数据库读取(数据库值 → Go 值)
type Scanner interface {
	Scan(src interface{}) error
}

// Valuer:写入数据库(Go 值 → 数据库值)
type Valuer interface {
	Value() (driver.Value, error)
}

GORM 完全兼容这两个接口。实现它们,你就能自定义任何类型在数据库中的存储方式。


29.2 自定义 JSON 类型 ​

go
import (
	"database/sql/driver"
	"encoding/json"
	"fmt"
)

// JSONMap:存到数据库是 JSON 字符串,Go 里是 map
type JSONMap map[string]interface{}

// Scan:从数据库读取 JSON 字符串 → 还原为 map
func (j *JSONMap) Scan(value interface{}) error {
	if value == nil {
		*j = make(JSONMap)
		return nil
	}
	bytes, ok := value.([]byte)
	if !ok {
		return fmt.Errorf("无法转换 %T 为 JSONMap", value)
	}
	return json.Unmarshal(bytes, j)
}

// Value:Go 的 map → 序列化为 JSON 字符串存入数据库
func (j JSONMap) Value() (driver.Value, error) {
	if j == nil {
		return nil, nil
	}
	return json.Marshal(j)
}

使用:

go
type User struct {
	ID       uint
	Name     string
	Settings JSONMap `gorm:"type:json"`
}

user := User{
	Name: "张三",
	Settings: JSONMap{
		"theme": "dark",
		"lang":  "zh",
		"notify": true,
	},
}
db.Create(&user)

// Settings 自动存储为: {"theme":"dark","lang":"zh","notify":true}
// 读取时自动还原为 map

29.3 自定义枚举类型 ​

go
type Gender string

const (
	GenderMale   Gender = "male"
	GenderFemale Gender = "female"
	GenderOther  Gender = "other"
)

func (g *Gender) Scan(value interface{}) error {
	if value == nil {
		*g = ""
		return nil
	}
	switch v := value.(type) {
	case []byte:
		*g = Gender(v)
	case string:
		*g = Gender(v)
	}
	return nil
}

func (g Gender) Value() (driver.Value, error) {
	return string(g), nil
}

29.4 自定义加密类型 ​

go
type EncryptedString string

var encryptionKey = []byte("0123456789abcdef") // 16字节密钥

// Value:写入数据库前加密
func (es EncryptedString) Value() (driver.Value, error) {
	if es == "" {
		return "", nil
	}
	encrypted, err := aesEncrypt([]byte(es), encryptionKey)
	if err != nil {
		return nil, err
	}
	// 返回 Base64 编码的密文
	return base64.StdEncoding.EncodeToString(encrypted), nil
}

// Scan:从数据库读取后解密
func (es *EncryptedString) Scan(value interface{}) error {
	if value == nil {
		*es = ""
		return nil
	}
	var encryptedStr string
	switch v := value.(type) {
	case []byte:
		encryptedStr = string(v)
	case string:
		encryptedStr = v
	}
	encrypted, _ := base64.StdEncoding.DecodeString(encryptedStr)
	decrypted, err := aesDecrypt(encrypted, encryptionKey)
	if err != nil {
		return err
	}
	*es = EncryptedString(decrypted)
	return nil
}

使用:

go
type User struct {
	ID     uint
	Name   string
	Secret EncryptedString `gorm:"type:text"` // 存入数据库自动加密
}

user := User{Name: "张三", Secret: "这是一条秘密信息"}
db.Create(&user)  // 存入时自动加密

var found User
db.First(&found, user.ID)
fmt.Println(found.Secret)  // 读取时自动解密 → "这是一条秘密信息"

29.5 serializer 标签(简化方案) ​

很多场景不需要手写 Scanner/Valuer,GORM 内置了序列化器:

go
type UserSettings struct {
	Theme  string `json:"theme"`
	Lang   string `json:"lang"`
	Notify bool   `json:"notify"`
}

type User struct {
	ID       uint
	Name     string
	Settings UserSettings `gorm:"serializer:json"`   // 自动 JSON 序列化
	Tags     []string     `gorm:"serializer:json"`   // 切片自动 JSON
}
序列化器用途
serializer:jsonJSON 序列化
serializer:unixtimeUnix 时间戳
serializer:unixmilliUnix 毫秒时间戳
serializer:gobGob 二进制
自定义实现 SerializerInterface

29.6 选择建议 ​

场景推荐
struct/map/slice 存为 JSONserializer:json
需要复杂加密逻辑自定义 Scanner/Valuer
兼容已有代码自定义 Scanner/Valuer
枚举类型自定义类型 + Scan/Value
简单序列化serializer 标签

本章小结 ​

  • Scanner(读)和 Valuer(写)实现类型的自定义存储
  • serializer:json 是存储 struct/map/slice 的最简方案
  • 加密类型:Valuer 中加密,Scanner 中解密
  • GORM 兼容 database/sql 的 Scanner/Valuer 接口

练习题 ​

  1. 实现一个自定义 JSON 类型 JSONArray(存储为 JSON 数组字符串)。
  2. 实现一个 Status 枚举类型(active/inactive/banned)。
  3. 使用 serializer:json 存储用户偏好设置。
  4. 实现一个简单的加密字符串类型。
  5. (思考题)Scanner/Valuer 和 Serializer 有什么区别?为什么 GORM 要保留两套机制?

把零散知识,整理成可以反复走的路。