golang拾遗主要是用来记录一些遗忘了的、平时从没注意过的golang相关知识。

很久没更新了,我们先以一个谜题开头练练手:

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "time"
  6. )
  7. type MyTime time.Time
  8. func main() {
  9. myTime := MyTime(time.Now()) // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
  10. res, err := json.Marshal(myTime)
  11. if err != nil {
  12. panic(err)
  13. }
  14. fmt.Println(string(res))
  15. }

请问上述代码会输出什么:

  1. 编译错误
  2. 运行时panic
  3. {}
  4. "2022-07-20T20:30:00.135693011+08:00"

很多人一定会选4吧,然而答案是3:

  1. $ go run customize.go
  2. {}

是不是很意外,MyTime就是time.Time,理论上应该也实现了json.Marshaler,为什么输出的是空的呢?

实际上这是最近某个群友遇到的问题,乍一看像是golang的bug,但其实还是没掌握语言的基本规则。

在深入下去之前,我们先问自己两个问题:

  1. MyTime 真的是 Time 类型吗?
  2. MyTime 真的实现了 json.Marshaler 吗?

对于问题1,只需要引用spec里的说明即可:

A named type is always different from any other type.

https://go.dev/ref/spec#Type_identity

意思是说,只要是type定义出来的类型,都是不同的(type alias除外),即使他们的underlying type是一样的,也是两个不同的类型。

那么问题1的答案就知道了,显然MyTime不是time.Time

既然MyTime不是Time,那它是否能用Time类型的method呢?毕竟MyTime的基底类型是Time呀。我们写段代码验证下:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. type MyTime time.Time
  7. func main() {
  8. myTime := MyTime(time.Now()) // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
  9. res, err := myTime.MarsharlJSON()
  10. if err != nil {
  11. panic(err)
  12. }
  13. fmt.Println(string(res))
  14. }

运行结果:

  1. # command-line-arguments
  2. ./checkoutit.go:12:24: myTime.MarsharlJSON undefined (type MyTime has no field or method MarsharlJSON)

现在问题2也有答案了:MyTime没有实现json.Marshaler

那么对于一个没有实现json.Marshaler的类型,json是怎么序列化的呢?这里就不卖关子了,文档里有写,对于没实现Marshaler的类型,默认的流程使用反射获取所有非export的字段,然后依次序列化,我们再看看time的结构:

  1. type Time struct {
  2. // wall and ext encode the wall time seconds, wall time nanoseconds,
  3. // and optional monotonic clock reading in nanoseconds.
  4. //
  5. // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
  6. // a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
  7. // The nanoseconds field is in the range [0, 999999999].
  8. // If the hasMonotonic bit is 0, then the 33-bit field must be zero
  9. // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
  10. // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
  11. // unsigned wall seconds since Jan 1 year 1885, and ext holds a
  12. // signed 64-bit monotonic clock reading, nanoseconds since process start.
  13. wall uint64
  14. ext int64
  15. // loc specifies the Location that should be used to
  16. // determine the minute, hour, month, day, and year
  17. // that correspond to this Time.
  18. // The nil location means UTC.
  19. // All UTC times are represented with loc==nil, never loc==&utcLoc.
  20. loc *Location
  21. }

里面都是非公开字段,所以直接序列化后整个结果就是{}。当然,Time类型自己重新实现了json.Marshaler,所以可以正常序列化成我们期望的值。

而我们的MyTime没有实现整个接口,所以走了默认的序列化流程。

所以我们可以得出一个重要的结论:从某个类型A派生出的类型B,B并不能获得A的方法集中的任何一个

想要B拥有A的所有方法也不是不行,但得和type B A这样的形式说再见了。

方法一是使用type alias:

  1. - type MyTime time.Time
  2. + type MyTime = time.Time
  3. func main() {
  4. - myTime := MyTime(time.Now()) // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
  5. + var myTime MyTime = time.Now() // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
  6. res, err := json.Marshal(myTime)
  7. if err != nil {
  8. panic(err)
  9. }
  10. fmt.Println(string(res))
  11. }

类型别名自如其名,就是创建了一个类型A的别名而没有定义任何新类型(注意那两行改动)。现在MyTime就是Time了,自然也可以直接利用Time的MarshalJSON。

方法二,使用内嵌类型:

  1. - type MyTime time.Time
  2. + type MyTime struct {
  3. + time.Time
  4. + }
  5. func main() {
  6. - myTime := MyTime(time.Now()) // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
  7. + myTime := MyTime{time.Now}
  8. res, err := myTime.MarsharlJSON()
  9. if err != nil {
  10. panic(err)
  11. }
  12. fmt.Println(string(res))
  13. }

通过将Time嵌入MyTime,MyTime也可以获得Time类型的方法集。更具体的可以看我之前写的另一篇文章:golang拾遗:嵌入类型

如果我实在需要派生出一种新的类型呢,通常在我们写一个通用模块的时候需要隐藏实现的细节,所以想要对原始类型进行一定的包装,这时该怎么办呢?

实际上我们可以让MyTime重新实现json.Marshaler

  1. type MyTime time.Time
  2. func (m MyTime) MarshalJSON() ([]byte, error) {
  3. // 我图方便就直接复用Time的了
  4. return time.Time(m).MarshalJSON()
  5. }
  6. func main() {
  7. myTime := MyTime(time.Now()) // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
  8. res, err := myTime.MarsharlJSON()
  9. if err != nil {
  10. panic(err)
  11. }
  12. fmt.Println(string(res))
  13. }

这么做看上去违反了DRY原则,其实未必,这里只是示例写的烂而已,真实场景下往往对派生出来的自定义类型进行一些定制,因此序列化函数里会有额外的一些操作,这样就和DRY不冲突了。

不管哪一种方案,都可以解决问题,根据自己的实际需求做选择即可。

总结

总结一下,一个派生自A的自定义类型B,它的方法集中的方法只有两个来源:

  • 直接定义在B上的那些方法
  • 作为嵌入类型包含在B里的其他类型的方法

而A的方法是不存在在B中的。

如果是从一个匿名类型派生的自定义类型B(type B struct {a, b int}),那么B的方法集中的方法只有一个来源:

  • 直接定义在B上的那些方法

还有最重要的,如果两个类型名字不同,即使它们的结构完全相同,也是两个不同的类型

这些边边角角的知识很容易被遗忘,但还是有机会在工作中遇到的,记牢了可以省很多事。

golang拾遗:自定义类型和方法集的更多相关文章

  1. go语言之进阶篇指针类型和普通类型的方法集

    方法集 类型的方法集是指可以被该类型的值调用的所有方法的集合. 用实例实例 value 和 pointer 调用方法(含匿名字段)不受方法集约束,编译器编总是查找全部方法,并自动转换 receiver ...

  2. MyBatis-xml配置SQL文件中,传入List数组、基本类型String、int……、与自定义类型的方法

    //基本类型 @Override public String queryItemNumber(String packId) throws Exception { // TODO Auto-genera ...

  3. Javascript标准类型的方法集

    1 array.concat(item...) concat方法会产生一个新数组,将一个或多个item附加在数组之后 var a = ['a', 'b', 'c'] var b = ['x', 'y' ...

  4. golang拾遗:嵌入类型

    这里是golang拾遗系列的第三篇,前两篇可以点击此处链接跳转: golang拾遗:为什么我们需要泛型 golang拾遗:指针和接口 今天我们要讨论的是golang中的嵌入类型(embedding t ...

  5. golang 自定义类型的排序sort

    sort包中提供了很多排序算法,对自定义类型进行排序时,只需要实现sort的Interface即可,包括: func Len() int {... } func Swap(i, j int) {... ...

  6. golang拾遗:指针和接口

    这是本系列的第一篇文章,golang拾遗主要是用来记录一些遗忘了的.平时从没注意过的golang相关知识.想做本系列的契机其实是因为疫情闲着在家无聊,网上冲浪的时候发现了zhuihu上的go语言爱好者 ...

  7. golang sync.noCopy 类型 —— 初探 copylocks 与 empty struct

    问题引入 学习golang(v1.16)的 WaitGroup 代码时,看到了一处奇怪的用法,见下方类型定义: type WaitGroup struct { noCopy noCopy ... } ...

  8. Javascript 中创建自定义对象的方法(设计模式)

    Javascript 中创建对象,可以有很多种方法. Object构造函数/对象字面量: 抛开设计模式不谈,使用最基本的方法,就是先调用Object构造函数创建一个对象,然后给对象添加属性. var ...

  9. 《Go语言实战》Go 类型:基本类型、引用类型、结构类型、自定义类型

    Go 语言是一种静态类型的编程语言,所以在编译器进行编译的时候,就要知道每个值的类型,这样编译器就知道要为这个值分配多少内存,并且知道这段分配的内存表示什么. 提前知道值的类型的好处有很多,比如编译器 ...

随机推荐

  1. IIS发布Https和Https的问题

    asp.net调试页面的时候遇到一个问题,我喜欢右键点击在浏览器查看页面,打开的页面默认是https的,其实iis会同时生成http和https两种页面,但是我懒得每次去点.问题是页面中测试接口是ht ...

  2. 一篇文章说清 webpack、vite、vue-cli、create-vue 的区别

    webpack.vite.vue-cli.create-vue 这些都是什么?看着有点晕,不要怕,我们一起来分辨一下. 先看这个表格: 脚手架 vue-cli create-vue 构建项目 vite ...

  3. Linux嵌套目录权限的比较探究

    在/tmp目录下新建一个嵌套目录,名字分别为test_0.test_1.test_2.在test_2目录下新建普通文件,名为tryme.设置test_0和test_2的权限为777,设置test_1的 ...

  4. JS 异步与 Promise

    JS 异步与 Promise 本文写于 2020 年 6 月 8 日 1. 同步与异步与回调函数 Promise 现在是前端面试必考题呀,但是先不急着看 Promise,我们首先来看看什么是异步. - ...

  5. DingtalkChatbot简单使用

    DingtalkChatbot 前言:该项目配合钉钉机器人 ,手机用户可以通过面对面建群创建单人群聊然后在电脑端 ···->智能群助手->添加机器人->自定义-> 然后添加机器 ...

  6. C程序设计(谭浩强)第五版课后题答案 第一章

    大家好,这篇文章分享了C程序设计(谭浩强)第五版课后题答案,所有程序已经测试能够正常运行,如果小伙伴发现有错误的的地方,欢迎留言告诉我,我会及时改正!感谢大家的观看!!! 1.什么是程序?什么是程序设 ...

  7. git rename branch

    git 不能直接重命名远程分支,如果需要重命名则执行以下步骤操作: 重命名本地分支 删除远程分支 推送本地分支(重命名后的)到远程 额外说明: 1. 重命名后的分支也会保留历史 commit(应该是本 ...

  8. Cubieboard安装系统

    2013年买的一个小玩意. 一.硬件 1.1 相关资料 http://www.cubieforums.com http://cubie.cc 1.2 cubieboard1 1.3 无线网卡 水星 M ...

  9. 造个海洋球池来学习物理引擎【Three.js系列】

    github地址:https://github.com/hua1995116/Fly-Three.js 大家好,我是秋风.继上一篇<Three.js系列:   游戏中的第一/三人称视角>今 ...

  10. Spring bean到底是如何创建的?(上)

    前言 众所周知,spring对于java程序员来说是一个及其重要的后端框架,几乎所有的公司都会使用的框架,而且深受广大面试官的青睐.所以本文就以常见的一个面试题"spring bean的生命 ...