Golang Go语言中 json.Marshal 的使用体验不佳

发布于 1周前 作者 songsunli 来自 Go语言

我的需求是,输入 sql ,返回序列化后的 json 结果。

在 python 中,官方库就可以返回[{'col1': 1, 'col2': '2', 'col3': true}....] 这种带类型的 json 结果。

在 go 中如果已知 sql 返回的列数和列类型,也可以构造一个 struct 进行数据映射,然后用json.Marshal转为 json 。

如果 sql 返回的列数和行类型未知,就很难受了,在 go/mysql 的官方 wiki 案例 中对于匿名的结果,使用了 interface 或者 sql.RawBytes ,但这两种替代方式在json.Marshal后都变成了 base64 encode 后的 string ,既丢失了类型也变异了结果( https://stackoverflow.com/questions/32501784/the-sqlx-library-gives-weird-base64-encoded-looking-results)

请问各位在实际业务中遇到这个问题是怎么处理的?

在其他语言中很自然的 object 序列化为原类型,在 go 的 json.Marshal 中怎么就全变成 string 了


Golang Go语言中 json.Marshal 的使用体验不佳

更多关于Golang Go语言中 json.Marshal 的使用体验不佳的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

101 回复

更多关于Golang Go语言中 json.Marshal 的使用体验不佳的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


没遇到过,没看懂

如果是不知道指针的类型,可以用反射啊

我翻译一下。

up 想要 go 实现 php 的 json_encode(PDO->fethcAll()) 不定义 struct

首先 Marshal 是序列化,Unmarshal 是反序列化, 也就是从 byte 到 go struct 应该用 Unmarshal
其次,对于未知的返回类型一般不 Unmarshal , 直接从 interface 里取键值,go 自带返回 ok 判断是否有这个 key
譬如
go<br>type SqlResult map[string]interface{}<br>var res SqlResult<br>if col1,ok := res["col1"].(int32); ok {<br>//...<br>}<br>

你适合用 python

根据 ColumnType 判断一下字段类型,自己写个转换,把[]byte 和[]uint8 都转成 string

你适合用 PHP

这不是 go 和 python 的区别,是强类型语言与弱类型语言的区别

其实 python 也是强类型语言

除了 PHP ,Python 和 Java 也行,这件事情上,唯独 Go 比较大道至简

你可以用 map[string]any 或 json.RawMessage

map[string]any, []any…族繁不及备载

要修改下结构体定义, 例如把 []uint8 换成 []uint16

看 col 举例应该是[]map[string]any

OP 赶紧跑吧你完了

最开始用 py 或者 php 的来写 go ,很容易有代入感,经常看到别人把 go 写成 php

收收 python 、js 、php 味。go 的正规业务里,都要得先定义 struct ,不到万不得已不用 map[string]any

总结不适应强类型

go 真冤 完全没看懂这跟 go 有什么关系

我去查了下 grafana 的代码,又查了下 sof ,对于未知返回类型的查询,的确是对各个类型自己去重新实现映射/序列化

和 说的差不多

但是 java, c#这种都可以 getObject,然后直接对 Object 序列化出带类型的 json

但是 go 语言中,用 interface 或者 any 来模拟 java c#这种 getObject ,然后序列化,结果就是 base64 encode 后的 string

既然用 sql 了,为啥还会存在“行类型未知”这种场景?

是最好定义 struct 外加上泛型

肯定是有传入黑箱 sql ,返回 json 结果的场景,没遇到不等于没有。这和 python 、js 、php 味没关系,java c#都正常

标准的 JSON 不是只能使用 “” 双引号包围的任意数量 Unicode 字符的集合吗?

用 java 模拟 mysql 命令行工具,这个场景是不是 sql 已知,行类型未知?



你不能直接就一个 any 最起码知道它是字典阵列 不知道就先 json.RawMessage 根据内容 parse 一下用字典或阵列装

你适合用 PHP 典型的脚本语言思想

阿… Marshal 的话就直接 Marshal 成 json.RawMessage 就好了
把字段定义成 json.RawMessage 即可

怎么可能是 base64 后的 string 啊, 贴个例子看看. 八成是 unicode 吧, 提醒一下, json 必须是 utf8 编码的.

在 go/sql 中,interface 或者 any 会返回[]byte ,在 json.Marshal 中,[]byte 会被序列化成 base64 encode 字符串


Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string, and a nil slice encodes as the null JSON value.

https://forum.golangbridge.org/t/json-encode-byte-array-as-hex-string-instead-of-base64/26751

<br>One must use reflection to walk and copy the original object replacing byte arrays with the user-defined type for which the MarshalJSON method is defined, but there’s probably no better way. An even uglier alternative is to post-process the JSON.<br>

犯懒就用 js python 就好了,何必选个强类型语言给自己找罪受呢

用一门新语言时,最怕就是拿别的语言对比新学的语言。

不想麻烦就定义结构体,一次定义全代码收益,要像脚本语言那么自然,就得多写一些代码转换。

1 、项目定型是这个语言。2 、类似的蹩脚问题不止这一个,在 go1.8 之前代码里面好多 inteface 飞来飞去,不仅丢失了类型还影响类型安全。 我讨厌 interface 转来转去也是犯懒,我觉得 go/sql 和 json.Marshal 对于匿名类型查询不友好也是犯懒?你扣帽子是什么态度?

因为 json 的 string 必须为 UTF-8 而[]byte 不一定是 UTF-8

你可以试试 map any any

不过建议你写 php

同样是使用匿名类型去序列化,c#正确识别出了 Number 类型

c#<br>// See <a target="_blank" href="https://aka.ms/new-console-template" rel="nofollow noopener">https://aka.ms/new-console-template</a> for more information<br>using System.Text.Json;<br>using MySqlConnector;<br><br><br>using var connection = new MySqlConnection("Server=127.0.0.1;User ID=root;Password=****;Database=test");<br>connection.Open();<br><br>using var command = new MySqlCommand("select id from t1;", connection);<br>using var reader = command.ExecuteReader();<br>var x = new Dictionary&lt;string, object&gt; { };<br>while (reader.Read())<br>{<br> x["name"] = "name";<br> x["value"] = reader.GetValue(0);<br>}<br>Console.WriteLine(JsonSerializer.Serialize(x));<br>

输出结果为

js<br>{"name":"name","value":1}<br>

建议用 python

可以用 gjson

不然列,[]byte 本来就已经是 byte array 了,你再把他序列化一遍想变成啥?

sql.RawBytes 就是[]byte, []byte 序列化 json 的时候就是应该使用 base64 ,不然控制字符怎么打印?你不能认为都是[]byte{97, 98} ab 这种可打印字符。

不符合你的预期而已,你都用[]byte 接收据了,类型肯定丢失了。

life is short , use php



package main

import(
“encoding/json”
)

func main() {
m := map[string]any {
“name”: “name”,
“value”: 1,
}
b, _ := json.Marshal(m)
println(string(b))
}

{“name”:“name”,“value”:1}

惯性思维、学艺不精

总结:go 没问题,是你不会自适应

package main

import(
“encoding/json”
)

func main() {
m := map[string]any {
“name”: “name”,
“value”: json.RawMessage([]byte(“1”)),
}
b, _ := json.Marshal(m)
println(string(b))
}

{“name”:“name”,“value”:1}

看你的需求,直接存 string 字符串,查询的时候用 gjson 读。当然这样性能和安全就下降了一个层次



我已经回了 n 次 json.RawMessage…

定义跟 sql table 对应的 struct 是必要的, 自动绑定后用 json 去操作 struct 就没这个问题了.
个人觉得 orm 不好用, 自己搞了份 raw sql+自动绑定的库, 有兴趣可以试试, 例子:
https://github.com/lesismal/sqlw_examples/blob/main/mysql/db/db.go#L101



你写的这个楼主说的 sqlx 已经有了
然后我在公司又把它封装成无 join 功能的 orm

我觉得用一门语言的原则是用他的推荐方式去实现代码,而不是想着怎么这垃圾语言没法像谁谁那样写。。。

同样是使用匿名类型去序列化,java 也正确识别出了 Number 类型
java<br>package com.test.Demo;<br><br>import java.sql.*;<br>import <a target="_blank" href="http://java.io" rel="nofollow noopener">java.io</a>.*;<br>import java.util.*;<br>import com.google.gson.*;<br><br>public class Demo {<br> static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; <br> static final String DB_URL = "jdbc:mysql://127.0.0.1:3306/test?useSSL=false&amp;allowPublicKeyRetrieval=true&amp;serverTimezone=UTC";<br> static final String USER = "root";<br> static final String PASS = "******";<br><br> public static void main(String[] args) {<br> Connection conn = null;<br> Statement stmt = null;<br> try{<br> Class.forName(JDBC_DRIVER);<br> conn = DriverManager.getConnection(DB_URL,USER,PASS);<br> stmt = conn.createStatement();<br> String sql = "select id from t1";<br> ResultSet rs = stmt.executeQuery(sql);<br> HashMap&lt;String, Object&gt; x;<br> x = new HashMap&lt;String, Object&gt;();<br> <br> while(rs.next()){<br> Object id = rs.getObject("id");<br> x.put("name", "name");<br> x.put("value", id);<br> }<br> Gson gson = new Gson();<br> System.out.println(gson.toJson(x));<br> } catch (Exception e){<br> e.printStackTrace();<br> }<br><br> }<br><br>}<br><br>

map[string]any
然后根据你说的,某个标识字段,判断类型,再用 https://github.com/mitchellh/mapstructure
转成你要的实际 struct

这是 sql 库返回的对象不好序列化的问题,不是 json 库的问题,要解决的话也应该是 sql 库这边来处理,给一个方便打印的办法

json.RawMessage 是可行的,但是看起来和 java/c#的模式不同,从 go/sql 映射出来的 interface 变成了[]byte 而不是(interface).type ,而且官方文档好像也推荐用 interface 或者 sql.RawBytes 来接未知类型

是的,我说错了,是 go/sql 返回的 interface 无法直接序列化

遇到过这个包在弱类型解析时有精度丢失的情况

我猜想要的效果是这样的:
python<br>import pymysql<br>import pymysql.cursors<br><br><br>connect = pymysql.connect(<br> host="<a target="_blank" href="http://example.com" rel="nofollow noopener">example.com</a>", port=3306, user="user", password="password", database="database_name",<br> cursorclass=pymysql.cursors.DictCursor,<br>)<br>with connect.cursor() as cursor:<br> cursor.execute(<br> """select * from users limit %(param_limit)s""",<br> args={"param_limit": 2}<br> )<br> results = cursor.fetchall()<br> print(results)<br> # [<br> # {'id': 13, 'birthday': <a target="_blank" href="http://datetime.date" rel="nofollow noopener">datetime.date</a>(1900, 1, 1), 'create_time': datetime.datetime(2020, 9, 3, 16, 56, 39)},<br> # {'id': 39, 'birthday': <a target="_blank" href="http://datetime.date" rel="nofollow noopener">datetime.date</a>(1900, 1, 1), 'create_time': datetime.datetime(2020, 9, 14, 17, 5, 1)}<br> # ]<br>



func TestMy(t *testing.T) {
db, err := sql.Open(“mysql”, “root:123/test”)
if err != nil {
t.Fatal(err)
}
defer db.Close()

rows, err := db.Query(“SELECT id FROM t1”)
if err != nil {
t.Fatal(err)
}

for rows.Next() {
var id any
if err := rows.Scan(&id); err != nil {
t.Fatal(err)
}
m := map[string]any{
“value”: id,
}
b, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
t.Log(string(b))
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
}

{“value”:1}


用 any 接收数据,返回的类型由驱动实现决定,database/sql 只提供接口,驱动负责返回 sqldriver.Value ,所以真不是 Go 语言的问题。

go 的语法很怪异

这跟 json 都没有关系,而是 sql 类型和 go 类型之间的转换问题。

你这个是什么驱动?


1. orm 是非常不好的选择, 中小项目倒是还可以, 大项目大数据量的, 用 orm 存在一些不确定性可能导致性能风险, 所以我都是禁止团队试用 golang orm 的
2. 还有生成代码的 sqlc 这种, 性能当然是最友好但用起来也有点麻烦, 因为一些业务是上下文比较复杂的, 单纯生成一段 sql 对应的 go 代码还需要 ctrl cv 而且还要修改对应的地方, 功能修改的时候比较麻烦
3. sqlx 简化了一些 binding, 但也把一些简单的复杂化了, 比如又有 Select 又有 Get, 比如 Insert 缺少 struct 的自动绑定(我没深入使用只是看它文档和例子), 综合下来我觉得它少了一些可以真正节约体力的但多了不少没必要的, 所以对我来说病不好用

所以以前还是继续标准库 raw sql, 直到我实在忍不了标准库 raw sql 自己搞了这个

#37 没看到别人扣帽子,反而是你自己情绪化,人家都说了你用不惯 go 就换成你喜欢的语言好了,再说了,interface 满天飞就是犯懒的行为。

1.8 之前还有什么办法避免 Interface 满天飞?

我这个就是简简单单几点:
1. 鼓励明确定义 table 对应的 struct, 用明确定义的 struct 操作 sql, 这个 struct 自己手写也好用工具生成也好, 都挺方便的
2. 鼓励 raw sql, 避免 orm 之类的可能生成了性能不友好的语句造成性能事故
3. 框架尽量自动映射/绑定 sql table 和 struct, insert/update/select 这些与 struct 映射绑定操作密切的都很轻松用 struct 操作
4. 奥卡姆剃刀原则, 不提供 orm, 不提供那么多没什么必要的垃圾接口

"我觉得 go/sql 和 json.Marshal 对于匿名类型查询不友好也是犯懒?"
不友好的意思是指不能用方便又稳定的办法解决这个问题,只能用麻烦但是稳定的方式解决.
你认为这不友好难道不是犯懒吗?
BTW,何必这么敏感,犯懒又不是什么攻击性的词语,我自己也经常犯懒,人皆是如此,只是要可能会付出代价
这个世界还是挺美好的,没必要一进入互联网就进入攻击模式

你想表达的是动态语言和静态语言的区别

犯懒字面上是贬义词吧,你觉得不是攻击性的,那你可以自己多读几遍,不要替别人做决定。稳定不稳定不知道,写起来麻烦的代码后面维护起来肯定更麻烦

#71
interface 满天飞是当时写这些垃圾代码的人的问题, 不是 golang 自己的问题.
正确姿势本来就应该避免 interface 满天飞, 我看到很多喜欢搞 interface 满天飞的, 是 php nodejs 之类的转 go 的人居多, 这也不能全怪他们, 毕竟以前语言习惯已经信手拈来了, 刚开始未必能意识到这样会带来工程上的问题, 更何况有时候赶工只追求先搞定业务
c/cpp 或者一些新手上路就是 go 的, 姿势正常的人多些

接手别人的屎山不是 OP 的错, interface 满天飞不是 golang 的错, OP 要怪就都怪写屎山的人吧

用 ScanType 反射?

用 go 千万不要和别的语音比 不要之前语言思维定势。不然坑还更多

按楼上说的 json.RawMessage 解决了,我没有想到要去 json 命名空间里找类型



这你要问 driver 为什么输出的格式是[]byte 也就是你上面贴的那个 lib
目前没时间研究 你自立自强点
至於 json 对[]byte 处理是合理的



我目前弄的只有简单的 crud 和建立表 join 没整其他指令也没整 还是基於 sqlx 整的 只是用原生的 sql 应该也都差不多
根据 struct 中 tag 讯息拼接字符串 反射方法都已经写好 验证下来没问题 会这样搞主要是想偷懒点

嫌麻烦而想放弃,这不是犯懒?

回帖不支持代码,,看着就非常难受!

show me the code

这边建议转 rust 试试

Python 是强类型语言,不管用 ORM 还是原生 SQL 一样要处理类型问题。

不懂某些评论为什么要说有 Python 味,吐槽之前好好调查一下再给出观点很难是吗?


“1. orm 是非常不好的选择, 中小项目倒是还可以, 大项目大数据量的, 用 orm 存在一些不确定性可能导致性能风险, 所以我都是禁止团队试用 golang orm 的”

这一点可以举例说明:有哪些不确定性可能导致性能风险吗

我现在在公司用的就是 GORM ,想了解一下

go 的 sql set 0 的问题太带劲了

你满意就好

幸好不会用 java 、C#、C 、C++

grom 可以设置 Logger level 在开发过程中就可以看到执行的 sql ,有问题能直接发现。gorm 也有 raw sql 模式可以直接用。
是在使用 GROM 中踩了哪些坑么。如果有坑的话,我需要重新考虑下要不要用 gorm

Go 使用 json.RawMessage 也可以达到你说的效果,不会变成 base64

我是这么用的。

我就是好奇,这些人张嘴闭嘴就是 ORM 有不确定性,到底有什么不确定性,自己测试过?还是踩过坑?还是说嘴一张一闭反正我不管,ORM 就是有性能问题?

我之前进行过性能测试,如果 GORM 那些事务什么的全都关了,是会比原生 SQL 慢一些,但是没有慢特别多。至今没有踩过什么坑。



orm 本身没有不确定性 本质还是产生 query string 传参 orm 的不确定性在於框架的实现 太过细节的框架从另外角度讲是坑
gorm 确实是慢没错 更何况就是要用事务
我离职了再写一个比较好的自己用

各位,我比对了下,[]interface{}没有被 json.Marshal 出正确的数值和类型,是与我用的 go-mysql-driver 版本有关。

1.6.0 版本是出现了与我预测不同的结果。1.8 版本出现了预测的结果

json.Marshal 可能与本次贴文无直接关系。

另外,我觉得问题出自 json.Marshal 是从这个 sof 链接中的评论得到的错误推论,的确有迷惑性( https://stackoverflow.com/questions/34089750/marshal-byte-to-json-giving-a-strange-string)


<br>This is because some idiot Millennial at Google decided it. Breaking the behaviour the RFC their JSON impl claims to follow defines. Check <a target="_blank" href="http://stackoverflow.com/a/78662958/3768429" rel="nofollow noopener">stackoverflow.com/a/78662958/3768429</a> for details.<br>

该文指出 json.Marshal 处理 uint8[]时,错误输出了文字, 验证代码如下
<br>func main() {<br> var x = []uint8{1, 2, 3, 4, 5, 6}<br> var y = []int8{1, 2, 3, 4, 5, 6}<br> xBytes, err := json.Marshal(x)<br> if err != nil {<br> panic(err)<br> }<br> yBytes, err := json.Marshal(y)<br> if err != nil {<br> panic(err)<br> }<br> fmt.Println(fmt.Sprintf("uint8 %s, int8: %s", string(xBytes), string(yBytes)))<br>}<br>
输出的结果为
<br>uint8 "AQIDBAUG", int8: [1,2,3,4,5,6]<br>

https://go.dev/play/p/KGNG6voRuDk



ORM 的不确定性往往不是出自 ORM 本身,而是使用者的不确定性,大部分人只是会用,并不全面了解,有些时候会遇到令人惊讶的特性,其中不少人会专门写踩坑博文。

您是对的,这的确和 json.Marshal 没关系,是 go-mysql-driver 的问题。

https://github.com/go-sql-driver/mysql/pull/1424



vczyh 先讲这问题的 后面才看到 你要先感谢他



我有跳转过去 go 底层看就是 基本类型应该都有函盖才对 所以推测 driver 问题 不过没验过 对照一下 vczyh 的说明应该是这样

以前是用别的语言的吧,我记得有个库可以用比原生的优雅一点,不习惯的话要不要问问别人怎么用的再来吐槽

话说对于 JSON 解析如果没啥特殊需求,大多数语言都有 jmespath 的分支吧,以前用 py 的版本发现性能秒杀同类 jsonpath/objectpath 而且语法也简单,没必要自己硬抠嵌套结构,json-handler 一开直接提取路径了

在Go语言中,json.Marshal 函数是标准库 encoding/json 提供的一个用于将Go的结构体、切片、映射等数据类型序列化为JSON格式的工具。尽管它在功能上是相当强大的,但一些开发者可能会在使用过程中遇到一些体验上的不便,以下是一些可能的改进建议及说明:

  1. 错误处理json.Marshal 返回两个值,一个是序列化后的字节切片,另一个是可能发生的错误。开发者需要始终检查错误值,这在某些情况下可能会让代码显得冗长。使用类似 if err != nil 的错误检查模式可以提高代码的健壮性,但也会增加代码量。

  2. 字段标签:虽然Go结构体可以通过标签(tags)来控制JSON序列化的输出,但这需要开发者在定义结构体时就做好规划,对于需要动态调整序列化输出的场景,这可能不够灵活。

  3. 性能考虑:对于大规模数据的序列化,json.Marshal 的性能可能不是最优的。虽然大多数场景下它的性能已经足够好,但在一些高性能要求的场景下,可能需要考虑其他优化手段。

  4. 复杂性:对于复杂的数据结构,特别是嵌套多层的情况,直接使用 json.Marshal 可能会产生不太直观的JSON输出,这需要通过额外的代码逻辑来调整。

为了改善使用体验,开发者可以考虑使用第三方库或者自定义封装函数来简化操作,提高代码的可读性和可维护性。同时,持续关注Go社区对标准库的更新和优化也是提高使用体验的一个重要途径。

回到顶部