GO处理MySQL JSON的三种方案该选哪个

 更新时间:2026年09月18日 10:05:31   作者:kevin_tech  
这篇文章主要介绍了GO处理MySQL JSON的三种方案该选哪个的相关资料,文中通过示例代码详细对比GORM中JSON处理的三种方案,需要的朋友可以参考下

前言

大家好,最近在维护一些先人写的代码时,发现很多先人在处理 MySQL 的JSON 字段时,习惯性地用 string类型。好像没啥毛病,反正存进去取出来再 Unmarshal一下也能用。

但实际上,自从 MySQL 5.7 官方不再维护后,MySQL 8.x 版本中,JSON 字段的功能和性能已经不可同日而语了。如果我们还在用老一套的“字符串一把梭”,不仅浪费了数据库的能力,还给代码埋下了隐患。

今天我们就以设计一个“作家(Writer)”表为例,聊聊在 GORM 中处理 JSON 数据的三种姿势。

假设我们要存储作家的信息,其中有两个典型的 JSON 字段:

  • milestones (创作年表):结构固定(年份+事件)。

  • metadata (元数据):结构不固定(可能包含流派、代理商、社交账号等)。

初始化 SQL 如下:

CREATE TABLE`writers` (
`id`INT AUTO_INCREMENT PRIMARY KEY,
`name`VARCHAR(100) NOTNULL,
`milestones`JSONDEFAULTNULLCOMMENT'创作年表',
`metadata`JSONDEFAULTNULLCOMMENT'元数据',
`created_at` DATETIME DEFAULTCURRENT_TIMESTAMP
);

方案 A:使用 string存储

这是最简单直接的写法,直接把数据库的 JSON 字段映射成 Go 的 string

Model 定义:

type WriterA struct {
    ID         int    `gorm:"primaryKey"`
    Name       string `gorm:"not null"`
    
    // 直接用 string,完全把 JSON 当字符串处理
    Milestones string `gorm:"column:milestones;type:json"`
    Metadata   string `gorm:"column:metadata;type:json"`
}

存取示例:

// 写入时:需要手动序列化,并强制转换为 string
milestones := []map[string]interface{}{
    {"year": 2006, "work": "三体"},
}
jsonBytes, _ := json.Marshal(milestones)
writer.Milestones = string(jsonBytes) //会发生内存拷贝

// 读取时:需要将 string 转回 []byte 再反序列化
var result []map[string]interface{}
json.Unmarshal([]byte(writer.Milestones), &result)

这个方案能用,但代码里充斥着类型转换,且性能和规范性最差。

方案 B:使用GORM的库 datatypes.JSON

这是 GORM 提供的方案,datatypes.JSON的底层其实是 []byte的别名。

Model 定义:

import "gorm.io/datatypes"

type WriterB struct {
    ID         int            `gorm:"primaryKey"`
    Name       string         `gorm:"not null"`
    
    // 使用官方类型
    Milestones datatypes.JSON `gorm:"column:milestones"` 
    Metadata   datatypes.JSON `gorm:"column:metadata"` 
}

存取示例:

// 写入时:依然需要序列化,但直接赋值 []byte
milestones := []map[string]interface{}{
    {"year": 2006, "work": "三体"},
}
jsonBytes, _ := json.Marshal(milestones)
writer.Milestones = datatypes.JSON(jsonBytes) // 零拷贝

// 读取时:直接反序列化
var result []map[string]interface{}
json.Unmarshal(writer.Milestones, &result)

string和 datatypes.JSON 的区别

很多同学会有疑惑:方案 A 和 方案 B,在业务代码里不都要手动 Marshal和 Unmarshal吗?看起来没区别啊?

其实在实际代码的执行过程中,方案 B 是对方案 A 的降维打击:

  • 查询能力的区别(关键)

  • 方案A (String):GORM 把它当普通字符串。如果你想查“所有科幻流派的作家”,你得手写原生 SQL (WHERE metadata->>'$.tag' = 'sci-fi'),容易写错且难以维护。

  • 方案B (Datatypes.JSON):GORM 官方包封装了 JSONQuery。你可以写出非常优雅的链式调用,自动兼容不同数据库语法:

// 查找 metadata 中 tags 包含 "Sci-Fi" 的记录
db.Where(datatypes.JSONQuery("metadata").HasKey("tags", "Sci-Fi")).Find(&writers)
  • 性能与内存的区别

  • 方案A (String):json.Marshal出来是 []byte,存入 string字段时发生了一次内存拷贝(Copy)。高并发下这是无谓的 GC 压力。

  • 方案B (Datatypes.JSON):底层就是 []byte零拷贝直接落库。

数据库兼容性

方案A (String):进行数据迁移AutoMigrate时,GORM 不知道你是 JSON,可能会建成 TEXT类型,导致无法享受 MySQL 8.0 的 JSON 索引优化。

方案B (Datatypes.JSON):GORM 明确知道这是 JSON,会自动映射为数据库的最佳 JSON 类型。

方案 C:自定义结构体 (Scanner/Valuer)

利用 Go 的接口机制,让 GORM 自动在 Go 结构体和数据库 JSON 之间转换。这是“强类型”的写法。

具体定义:

// 1. 定义具体业务结构
type Milestone struct {
    Year int    `json:"year"`
    Work string`json:"work"`
}
type Milestones []Milestone

// 2. 实现 sql.Scanner (读库:JSON -> Struct)
func (m *Milestones) Scan(value interface{}) error {
    return json.Unmarshal(value.([]byte), m)
}

// 3. 实现 driver.Valuer (写库:Struct -> JSON)
func (m Milestones) Value() (driver.Value, error) {
    return json.Marshal(m)
}

// 4. Model 使用
type WriterC struct {
    ID         int        `gorm:"primaryKey"`
    // 直接使用强类型,无需再做 json 转换
    Milestones Milestones `gorm:"column:milestones;type:json"`
    // 假设 Metadata 也用结构体(如果结构固定的话)
    // Metadata MetadataStruct `gorm:"column:metadata;type:json"`
}

存取示例:

// 写入:直接赋值结构体!
writer.Milestones = Milestones{
    {Year: 2006, Work: "《三体》"},
}
db.Create(&writer)

// 读取:直接使用结构体!
var w WriterC
db.First(&w, 1)
fmt.Println(w.Milestones[0].Work) // 直接点出属性

三种方案横向对比

方案

类型

类型安全

灵活性

查询能力

推荐指数

适用场景

A: String

string

⭐⭐⭐

弱 (需手写SQL)

别用

(除非只做透传)

B: datatypes

[]byte

⭐⭐⭐

强 (官方支持)

⭐⭐⭐

动态结构

(如配置项、元数据)

C: 自定义结构

Struct

弱 (通常不查内部)

⭐⭐⭐⭐⭐

核心业务

(如年表、sku信息)

最佳实践:混合双打

看完对比,结论就很明显了。针对我们开头的 Writer表:

对于 milestones(创作年表):结构固定,使用方案 C,确保类型安全。

对于 metadata(元数据):结构动态,使用方案 B,保留灵活性,同时支持 JSON 查询。

最终混合方案代码

package model

import (
    "database/sql/driver"
    "encoding/json"
    "gorm.io/datatypes"
    "time"
)

// === 1. 固定结构定义 (对应方案 C) ===
type Milestone struct {
    Year  int    `json:"year"`
    Work  string`json:"work"`
    Award string`json:"award,omitempty"`
}
type Milestones []Milestone

// 实现 Scanner 接口
func (m *Milestones) Scan(value interface{}) error {
    bytes, ok := value.([]byte)
    if !ok {
        returnnil
    }
    return json.Unmarshal(bytes, m)
}

// 实现 Valuer 接口
func (m Milestones) Value() (driver.Value, error) {
    return json.Marshal(m)
}

// === 2. 混合 Model 定义 ===
type Writer struct {
    ID   int    `gorm:"primaryKey"`
    Name string`gorm:"not null"`
    
    // 强类型:创作年表 (方案 C)
    // 优势:代码自动提示,编译期检查,无需手动转换
    Milestones Milestones `gorm:"column:milestones;type:json"`
    
    // 灵活类型:元数据 (方案 B)
    // 优势:什么都能存,支持 GORM 的 JSON 查询语法
    Metadata   datatypes.JSON `gorm:"column:metadata;type:json"`
    
    CreatedAt  time.Time
}

// === 3. 实际使用体验 ===
func main() {
    // 创建
    writer := Writer{
        Name: "刘慈欣",
        
        // 方案 C 爽点:直接写结构体,不用 Marshal
        Milestones: Milestones{
            {Year: 2006, Work: "《三体》"},
            {Year: 2015, Work: "《三体》", Award: "雨果奖"},
        },
    }
    
    // 方案 B:处理杂项,还是需要手动 Marshal 一下
    meta := map[string]interface{}{
        "tags": []string{"硬科幻", "宏大叙事"}, 
        "fans": "磁铁",
    }
    jsonBytes, _ := json.Marshal(meta)
    writer.Metadata = datatypes.JSON(jsonBytes) // 零拷贝赋值
    
    db.Create(&writer)
    
    // --- 查询与使用 ---
    var result Writer
    db.First(&result, writer.ID)
    
    // 方案 C 爽点:直接读取,自带类型
    println(result.Milestones[1].Award) // 输出:雨果奖
    
    // 方案 B 爽点:复杂查询
    // 查找 metadata 中 tags 包含 "硬科幻" 的记录
    var list []Writer
    db.Where(datatypes.JSONQuery("metadata").HasKey("tags", "硬科幻")).Find(&list)
}

总结

核心数据用 Struct 换安全,扩展数据用 datatypes.JSON 更灵活,坚决不用 string 一把梭。个人总结的 Go 语言处理 MySQL JSON 字段的口诀╮(╯▽╰)╭。

接下来是卖课时间,Go项目实战课 + Vue的H5商城实战课。Go实战项目采用DDD分层架构设计,结合事件驱动编程等多个程序设计最佳实践,以一个在线商城为例实战演示项目的搭建和开发过程。

Vue项目为Go项目的服务端接口功能提供可视化操作,在学习效果上形成闭环。目前两门课一起购买优惠59元,只需239即可拿下,一站式搞定前后两端。

到此这篇关于GO处理MySQL JSON的三种方案该选哪个的文章就介绍到这了,更多相关GO处理MySQL JSON内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Redis BloomFilter布隆过滤器原理与实现

    Redis BloomFilter布隆过滤器原理与实现

    你在开发或者面试过程中,有没有遇到过 海量数据需要查重,缓存穿透怎么避免等等这样的问题呢?下面这个东西超棒,好好了解下,面试过关斩将,凸显你的不一样
    2022-10-10
  • Golang函数的使用技巧(结合代码示例)

    Golang函数的使用技巧(结合代码示例)

    函数是基本的代码块,用于执行一个任务,这篇文章主要介绍了Golang函数使用的相关资料,文中通过代码介绍的非常详细,对大家的学习或者工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2025-11-11
  • Golang 实现复制文件夹同时复制文件

    Golang 实现复制文件夹同时复制文件

    这篇文章主要介绍了Golang 实现复制文件夹同时复制文件,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • sublime安装支持go和html的插件

    sublime安装支持go和html的插件

    这篇文章主要介绍了sublime安装支持go和html的插件,需要的朋友可以参考下
    2015-01-01
  • 关于golang监听rabbitmq消息队列任务断线自动重连接的问题

    关于golang监听rabbitmq消息队列任务断线自动重连接的问题

    这篇文章主要介绍了golang监听rabbitmq消息队列任务断线自动重连接,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-03-03
  • Go语言数据结构之选择排序示例详解

    Go语言数据结构之选择排序示例详解

    这篇文章主要为大家介绍了Go语言数据结构之选择排序示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • 使用goland调试远程代码的操作步骤

    使用goland调试远程代码的操作步骤

    大家都知道如何在goland调试远程代码吗?今天小编给大家分享一篇教程帮助大家学习goland调试远程代码的操作步骤,感兴趣的朋友跟随小编一起看看吧
    2021-06-06
  • golang中cache组件的使用及groupcache源码解析

    golang中cache组件的使用及groupcache源码解析

    本篇主要解析groupcache源码中的关键部分, lru的定义以及如何做到同一个key只加载一次。缓存填充以及加载抑制的实现方法,本文重点给大家介绍golang中cache组件的使用及groupcache源码解析,感兴趣的朋友一起看看吧
    2021-06-06
  • golang 手写贪吃蛇示例实现

    golang 手写贪吃蛇示例实现

    这篇文章主要为大家介绍了golang 手写贪吃蛇示例实现,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • Go 1.18新特性之泛型的全面讲解

    Go 1.18新特性之泛型的全面讲解

    本文力求能让未接触过泛型编程的人也能较好理解Go的泛型,所以行文可能略显啰嗦。但是请相信我,看完这篇文章你能获得对Go泛型非常全面的了解
    2023-03-03

最新评论