标签 标准库 下的文章

JSON包新提案:用“omitzero”解决编码中的空值困局

本文永久链接 – https://tonybai.com/2024/09/12/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero

Go标准库是Go号称“开箱即用”的重要因素,而标准库中的encoding/json包又是标准库最常用的Go包。虽然其性能不是最好的,但好在由Go团队维护,对JSON规范兼容性好,且质量很高。但json包也不是没有“瑕疵”的,Go官方继math/rand/v2之后,也开启了encoding/json/v2的讨论,v2包含了对功能的增强,其中就包含了对空值编码的改进的考量,以及性能方面的优化。但json/v2毕竟还属于“长远”规划,当前版本的json包的问题也要修正和完善。

一个提出于2021年的issue近期被即将“功成身退”的Russ Cox接受(accept),该issue就当前json包对空值编码的“瑕疵”做了描述并提出了修正方案。本文就将针对这一问题以及其方案进行探讨,希望能帮助大家更好地理解该issue以及其对应的方案。

1. 问题溯源:omitempty的局限性

在encoding/json包中,omitempty标签是开发者控制JSON序列化行为的重要工具。它的设计初衷是允许开发者指定:当某个字段值为“空”时,在JSON编码过程中应该被忽略。然而,omitempty的空值定义存在一些固有的局限性。下面是json包中对omitempty的说明:

The “omitempty” option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.

总结一下,omitempty标签的判断逻辑如下:

  • 对于布尔类型:false被视为空
  • 对于数值类型:0被视为空
  • 对于字符串:”"(空字符串)被视为空
  • 对于指针、接口:nil被视为空
  • 对于数组、切片、map:长度为0被视为空

下面是一个完整的Go示例,展示了omitempty标签在不同类型上的应用:

package main

import (
    "encoding/json"
    "fmt"
)

type Example struct {
    BoolField      bool           `json:"bool_field,omitempty"`
    IntField       int            `json:"int_field,omitempty"`
    StringField    string         `json:"string_field,omitempty"`
    PointerField   *string        `json:"pointer_field,omitempty"`
    InterfaceField interface{}    `json:"interface_field,omitempty"`
    ArrayField     [0]int         `json:"array_field,omitempty"` // 空数组
    SliceField     []string       `json:"slice_field,omitempty"` // 空切片
    MapField       map[string]int `json:"map_field,omitempty"`   // 空地图
}

func main() {
    var nilString *string = nil

    example := Example{
        BoolField:      false,            // 布尔类型
        IntField:       0,                // 数值类型
        StringField:    "",               // 空字符串
        PointerField:   nilString,        // nil 指针
        InterfaceField: nil,              // nil 接口
        ArrayField:     [0]int{},         // 空数组
        SliceField:     []string{},       // 空切片
        MapField:       map[string]int{}, // 空地图
    }

    jsonData, err := json.Marshal(example)
    if err != nil {
        fmt.Println("Error marshalling example:", err)
    }
    fmt.Println(string(jsonData)) // 输出:{}
}

然而,这种预定义的”空”值判断逻辑并不能满足所有实际场景的需求。让我们来看几个具体的例子:

  • 空结构体问题
package main

import (
    "encoding/json"
    "fmt"
)

type Config struct {
    EmptyStruct struct{} `json:",omitempty"`
}

func main() {
    cfg := Config{}
    data, _ := json.Marshal(cfg)
    fmt.Println(string(data)) // 输出:{"EmptyStruct":{}}
}

我们看到:在这个例子中,尽管Config中的EmptyStruct字段是一个空结构体类型,且添加了omitempty标签,但它仍然出现在JSON输出中。

  • 零值结构体

除了空结构体,零值结构体也是目前omitempty标签语义覆盖不到的类型:

package main

import (
    "encoding/json"
    "fmt"
)

type ZeroStruct struct {
    A int
    B string
    C float64
}

type Config struct {
    ZeroStruct ZeroStruct `json:",omitempty"`
}

func main() {
    cfg := Config{}
    data, _ := json.Marshal(cfg)
    fmt.Println(string(data)) // 输出:{"ZeroStruct":{"A":0,"B":"","C":0}}
}

我们看到:即便ZeroStruct中各个类型的值都为零,且有了omitempty标签,json.Marshal依然输出了Config中的ZeroStruct字段。

  • time.Time类型的处理

在开发实践中,我们发现json对time.Time类型在omitempty下的处理也与“常理”不符,比如下面这个示例:

package main

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

type Event struct {
    Time time.Time `json:",omitempty"`
}

func main() {
    evt := Event{Time: time.Time{}} // 零值时间
    data, _ := json.Marshal(evt)
    fmt.Println(string(data)) // 输出:{"Time":"0001-01-01T00:00:00Z"}
}

我们看到:time.Time类型的零值依然被输出了。并且输出的是公元1年1月1日UTC时间。对于很多应用来说,这个时间并不具有实际意义,更合理的零值是”January 01, 1970 00:00:00 UTC”。

很显然,Gopher们希望json包能更好的处理上述情形。

2. 社区讨论与omitzero标签方案的确认

关于上述问题的解决方法,在Go社区引发了广泛讨论。不过大家普遍认为不要改变现有omitempty语义,那样会导致破坏性的change,无法向后兼容。

在讨论过程中,社区成员提出了一些其他的解决方案:

  • 允许MarshalJSON()方法返回nil来完全忽略某个字段

这个方案的优点是利用了已有的接口,不需要引入新的标签。但缺点是需要为每个支持零值的类型都实现MarshalJSON()方法。

  • 添加OmitJSONField方法

这个方案提议为每个类型添加一个OmitJSONField() bool方法,由开发者自己控制字段的忽略逻辑,该方案提供了很大的灵活性,但可能会导致JSON序列化逻辑过于分散。

最终,”omitzero”方案最终被认为是一个相对平衡的解决方案,因为它可以与现有的标签系统兼容,开发者可以很容易地将omitempty替换为omitzero,或者在需要的地方同时使用两者。此外,omitzero也保持了简洁性,相比其他需要大量代码修改的方案,omitzero只需要添加标签或实现一个方法(可选项)即可。

“omitzero”标签提案的核心内容是:在序列化时,”omitzero”选项指定如果字段值为零,则该结构体字段应被省略。如果该类型定义了IsZero bool方法,那么这个零值就通过IsZero方法来判断;否则是根据字段是否是零值(通过reflect.Value.IsZero判断)来判断。该omitzero选项在反序列化(unmarshal)时没有效果。如果同时指定了”omitempty”和”omitzero”,则字段是否被省略基于两者的逻辑或关系。 这将意味着,在省略切片时,omitzero会省略空指针切片,但对于长度为零的非空切片,则不会。对于time.Time类型,会省略time.Time{}。

此外,omitzero不强制你实现IsZero方法,但开发者可以利用IsZero方法来自由控制自定义类型在omitzero标签下是否会被省略。

一旦有了omitzero,我们就可以用它解决上面提到的问题(omitzero尚未实现,下面是伪代码):

  • 解决空结构体问题
type Config struct {
    EmptyStruct struct{} `json:",omitzero"`
}

cfg := Config{}
data, _ := json.Marshal(cfg)
fmt.Println(string(data)) // 输出:{}
  • 更好地处理time.Time类型
type Event struct {
    Time time.Time `json:",omitzero"`
}

evt := Event{Time: time.Time{}} // 零值时间
data, _ := json.Marshal(evt)
fmt.Println(string(data)) // 输出:{}
  • 自定义类型的”零值”判断
type CustomInt int

func (ci CustomInt) IsZero() bool {
    return ci <= 0 // 自定义零值判断逻辑
}

type Data struct {
    Value CustomInt `json:",omitzero"`
}

d := Data{Value: CustomInt(-1)}
data, _ := json.Marshal(d)
fmt.Println(string(data)) // 输出:{}

3. 小结

通过引入”omitzero”标签,Go语言在解决JSON编码中”空”值处理的痛点上迈出了重要一步。这个方案不仅满足了开发者对更灵活的”空”值定义的需求,还保持了与现有系统的兼容性。目前该omitzero的落地时间尚未确定,最早也要等到Go 1.24版本。此外,encoding/xml等也会效仿json包,增加omitzero标签。

此外,伴随着omitzero提案被接受,另外一个在2021年由Josh Bleecher Snyder提出的相关提案:proposal: cmd/vet: warn about structs marked json omitempty也被重新“唤醒”,针对该提案,目前社区在active discussions。

随着后续encoding/json/v2的到来,我们可以期待Go语言在数据序列化领域会有更出色的表现。这不仅将提升json编解码效率,还将为构建更加健壮和灵活的基于json的Go应用程序铺平了道路。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

从零开始编程:Go语言真的适合新手吗?

本文永久链接 – https://tonybai.com/2024/08/22/go-as-first-language

Go语言自诞生以来,一直以其简洁、高效和面向工程的特性受到开发者的青睐,尤其是在后端开发和并发编程方面,Go表现出了独特的优势。然而,作为一门以简单著称的语言,它是否适合作为编程初学者的第一门语言呢?笔者今天在Reddit上看到有人提出此类问题,也做了一些思考,这里就通过本文从多个角度来和大家一起探讨下这一问题。

我们先从Go适合作为第一门语言的特质说起。

1. Go语言的简洁性

在我的《Go语言精进之路第一卷》第3节“理解Go语言的设计哲学”中我就提到过:Go语言的设计哲学是“做减法”,拒绝走语言特性融合的道路,提供简单的用户界面,将复杂性留给语言自身的设计和实现者

这种简洁性让初学者能够更快地上手,将精力聚焦在理解编程的基本概念和程序的逻辑结构上,而不是被复杂的语法规则所困扰。例如,Go的语法简洁明了,没有太多冗余的语法糖(一个事情大多只有一种写法),使得代码更容易阅读、理解和维护。

此外,Go的静态类型系统提供了清晰的类型检查机制,这有助于初学者理解变量和类型的概念。与动态类型语言相比,Go的类型系统减少了初学者在学习过程中可能遇到的困惑。此外,Go仍保留了指针等底层概念,让学生在学习过程中还能够接触到内存管理和效率优化的基础知识,也便于学习数据结构与算法,为后续的进阶学习做好铺垫。

2. 并发编程的天然优势

随着多核处理器的普及,并发编程已经成为现代编程的重要技能。Go语言将并发编程作为核心特性,通过goroutines和channels提供了一种简洁直观的并发模型。相比于传统的线程管理,Go的并发模型更加易于理解和使用,这为初学者在学习并发编程时提供了极大的便利。

通过学习Go的并发编程模型,初学者能够及早掌握现代编程中的关键概念,为后续在各种领域的发展打下坚实的基础。

3. 学习曲线与实践应用

经过Go团队不懈的努力,Go语言的安装与使用过程已经非常简单明了了,可以说开箱即用,初学者只需几步即可搭建开发环境。这种低门槛的特性让初学者可以专注于编程本身,而不必花费过多时间在环境配置上。同时,Go强大的标准库也为初学者提供了丰富的资源,使他们可以在不依赖第三方库的情况下完成许多实际项目,无需与繁芜的第三方依赖“作斗争”。此外,Go程序编译速度极快,可以快速让学员获得反馈。

注:我的极客时间专栏《Go语言第一课》专栏的“03|配好环境:选择一种最适合你的Go安装方法”有对Go环境搭建的系统全面的讲解,欢迎订阅阅读。

4. 面向未来的选择

选择第一门编程语言不仅关乎当下的学习体验,更应该着眼于未来的发展。Go在保持简洁性的同时,涵盖了现代编程的核心概念,从并发编程到网络服务,从系统编程到云原生应用,Go都能胜任。这种全面的能力使得Go成为初学者为未来编程生涯奠定基础的理想选择。

此外,Go在业界的广泛应用也为学习者提供了良好的职业前景。掌握Go的初学者可能会在未来的就业市场中占据优势,特别是在后端开发和云计算等领域。

尽管Go有许多优势,但我们也需要正视其作为第一门编程语言所面临的挑战。接下来,我们就来看看这些挑战!

5. 现实中的挑战

虽然Go语言简单易学,但现实中,无论中外,以Go为第一门编程语言的初学者数量依然不多,这是为什么呢?笔者认为主要有如下几点原因:

首先,Go虽然简单,简化了许多编程概念,但与Python、Scratch等传统的编程入门语言相比,学习曲线依旧“陡峭”,对于完全没有编程经验的初学者而言,理解指针、并发等特性仍然需要一定的努力。此外,Go主要面向后端开发,在前端开发和图形界面支持方面较为有限,对于一些喜欢更直观地通过图形化的方式看到反馈的初学者来说,Go的满足度有欠缺,这可能会影响一些初学者的学习兴趣。

其次,与Python等语言相比,Go主要面对后端开发,在数据科学和机器学习等热门领域的应用相对有限(虽然目前已经在向AI应用开发领域大踏步前进),这可能会影响一些对这些领域感兴趣的初学者的选择。对于许多初学者来说,这些看似更“酷”的应用领域可能比Go所擅长的系统编程和网络服务更具吸引力。

再次,Go在编程教育中的应用仍然相对有限。传统的计算机科学课程多年来已经确立并一直依赖于Java、Python或C++等成熟的编程语言。更新课程设置、编写新的教材、培训教师都需要时间和资源,这种惯性使得Go难以迅速进入教育体系。

最后,相比其他语言(如Java、C++等),Go团队在教育领域的推广较少,缺乏针对教育机构的专门资源和支持(这让我想起了当年Sun在校园推广Java,微软在校园推广C#等,没有对比,就没有伤害),同时由于Go语言的设计更倾向于“实战派”,并不受“学院派”青睐,因此学术界对Go语言研究也相对较少,导致在高等教育机构的推广受限。

6. 小结:平衡与选择

尽管面临这些挑战,Go语言作为第一门编程语言的潜力仍不容忽视。Go语言以其简洁性、并发特性和面向工程的实用型设计,无疑是一个极具吸引力的选择。虽然它可能不是最容易上手的语言,但它提供了一个平衡的学习体验,既不会掩盖重要的编程概念,也不会像Rust那样陡峭得令人望而却步。

同时,我们也需要认识到,没有一种编程语言能够完美满足所有学习者的需求。Go的优势在某些领域可能是无可替代的,但在其他方面可能需要补充。因此,一个更平衡的方法可能是将Go作为编程教育的重要组成部分,而不是唯一选择。比如,对于今天的初学者来说,理想的方案可能是将Go作为主要学习对象,同时辅以其他语言(如Python)来弥补在特定领域的不足。无论选择哪种语言,保持好奇心和学习的热情才是成为优秀程序员的关键。

最后,Go要实现在编程教育中扮演更重要的角色,还需要多方面的努力。Go团队和Go社区还需加强在教育领域的推广,开发更多面向初学者的资源和工具,以激励教育机构将Go纳入课程体系,特别是在教授并发编程等概念时。

如果你对Go语言感兴趣,并且希望系统地学习如何从零开始编写Go代码,我在两年多之前在极客时间开设了“Go语言第一课”的专栏。 这个专栏专为Go语言新手设计,从基础概念到实战案例,逐步带你掌握Go语言的核心知识点。无论你是编程初学者,还是希望掌握一门新语言的开发者,这个专栏都能为你提供有价值的学习资源和实践指导。

img{512x368}


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:https://tonybai.com
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats