Golang拾遗之自定义类型和方法集详解

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

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

package main

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

type MyTime time.Time

func main() {
    myTime := MyTime(time.Now()) // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
    res, err := json.Marshal(myTime)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(res))
}

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

1.编译错误

2.运行时panic

3.{}

4."2022-07-20T20:30:00.135693011+08:00"

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

$ go run customize.go
 
{}

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

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

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

  • MyTime 真的是 Time 类型吗?
  • 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呀。我们写段代码验证下:

package main

import (
    "fmt"
    "time"
)

type MyTime time.Time

func main() {
    myTime := MyTime(time.Now()) // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
    res, err := myTime.MarsharlJSON()
    if err != nil {
            panic(err)
    }
    fmt.Println(string(res))
}

运行结果:

# command-line-arguments
./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的结构:

type Time struct {
        // wall and ext encode the wall time seconds, wall time nanoseconds,
        // and optional monotonic clock reading in nanoseconds.
        //
        // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
        // a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
        // The nanoseconds field is in the range [0, 999999999].
        // If the hasMonotonic bit is 0, then the 33-bit field must be zero
        // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
        // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
        // unsigned wall seconds since Jan 1 year 1885, and ext holds a
        // signed 64-bit monotonic clock reading, nanoseconds since process start.
        wall uint64
        ext  int64

        // loc specifies the Location that should be used to
        // determine the minute, hour, month, day, and year
        // that correspond to this Time.
        // The nil location means UTC.
        // All UTC times are represented with loc==nil, never loc==&utcLoc.
        loc *Location
}

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

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

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

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

方法一是使用type alias:

- type MyTime time.Time
+ type MyTime = time.Time

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

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

方法二,使用内嵌类型:

- type MyTime time.Time
+ type MyTime struct {
+     time.Time
+ }

func main() {
-   myTime := MyTime(time.Now()) // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
+   myTime := MyTime{time.Now}
    res, err := myTime.MarsharlJSON()
    if err != nil {
            panic(err)
    }
    fmt.Println(string(res))
}

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

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

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

type MyTime time.Time

func (m MyTime) MarshalJSON() ([]byte, error) {
    // 我图方便就直接复用Time的了
    return time.Time(m).MarshalJSON()
}

func main() {
    myTime := MyTime(time.Now()) // 假设获得的时间是 2022年7月20日20:30:00,时区UTC+8
    res, err := myTime.MarsharlJSON()
    if err != nil {
            panic(err)
    }
    fmt.Println(string(res))
}

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

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

总结

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

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

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

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

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

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

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

到此这篇关于Golang拾遗之自定义类型和方法集详解的文章就介绍到这了,更多相关Golang自定义类型 方法集内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • goalng 结构体 方法集 接口实例详解

    目录 一 前序 二 事出有因 errors.As 方法签名 三 结构体与实例的数据结构 1. 结构体类型 2. 实例 3 方法调用 3.1 方法表达式 3.2 值实例调用所有方法 3.3 指针实例调用所有方法 3.4 空指针无法调用值方法 四 接口 1 接口数据结构 2 接口赋值 值方法集 指针方法集 总结 一 前序 很多时候我们以为自己懂了,但内心深处却偶有困惑,知识是严谨的,偶有困惑就是不懂,很幸运通过大量代码的磨练,终于看清困惑,并弄懂了. 本篇包括结构体,类型, 及 接口相关知识,希望对

  • Golang中的自定义类型之间的转换的实现(type conversion)

    这里不讨论数值与字符串之间.或者整型与浮点型之间的转换.这里要讨论的是自定义类型之间的转换,这个转换与其他语言都不一样,而且在go的源码中也被大量使用. 这里列举两个实用的例子. 转换成实现了某个(些)接口的自定义类型 比如:sort包里面的IntSlice,是一个[]int的自定义类型,并且实现了sort.Interface接口,如下所示: type IntSlice []int // 实现sort.Interface接口的方法 func (p IntSlice) Len() int { re

  • django自定义非主键自增字段类型详解(auto increment field)

    1.django自定义字段类型,实现非主键字段的自增 # -*- encoding: utf-8 -*- from django.db.models.fields import Field, IntegerField from django.core import checks, exceptions from django.utils.translation import ugettext_lazy as _ class AutoIncreField(Field): description =

  • golang对自定义类型进行排序的解决方法

    前言 Go 语言支持我们自定义类型,我们大家在实际项目中,常常需要根据一个结构体类型的某个字段进行排序.之前遇到这个问题不知道如何解决,后来在网上搜索了相关问题,找到了一些好的解决方案,此处参考下,做个总结吧. 由于 golang 的 sort 包本身就提供了相应的功能, 我们就没必要重复的造个轮子了,来看看如何利用 sort 包来实现吧. sort包浅谈 golang中也实现了排序算法的包sort包,sort 包 在内部实现了四种基本的排序算法:插入排序(insertionSort).归并排序

  • go语言方法集为类型添加方法示例解析

    目录 1概述 2为类型添加方法 2.1基础类型作为接收者 2.2结构体作为接收者 3值语义和引用语义 4方法集 4.1类型 *T 方法集 4.2类型 T 方法集 5匿名字段 5.1方法的继承 5.2方法的重写 6方法值和方法表达式 6.1方法值 6.2方法表达式 1概述 在面向对象编程中,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些函数,这种带有接收者的函数,我们称为方法(method).本质上,一个方法则是一个和特殊类型关联的函数. 一个面向对象的程序会用方法来表达其属性

  • Golang拾遗之指针和接口的使用详解

    目录 指针和接口 golang的指针 指向interface的指针 总结 指针和接口 golang的类型系统其实很有意思,有意思的地方就在于类型系统表面上看起来众生平等,然而实际上却要分成普通类型(types)和接口(interfaces)来看待.普通类型也包含了所谓的引用类型,例如slice和map,虽然他们和interface同为引用类型,但是行为更趋近于普通的内置类型和自定义类型,因此只有特立独行的interface会被单独归类. 那我们是依据什么把golang的类型分成两类的呢?其实很简

  • Element-ui tree组件自定义节点使用方法代码详解

    工作上使用到element-ui tree 组件,主要功能是要实现节点拖拽和置顶,通过自定义内容方法(render-content)渲染树代码如下~ <template> <div class="sortDiv"> <el-tree :data="sortData" draggable node-key="id" ref="sortTree" default-expand-all :expand-

  • mybatis自定义类型处理器TypehHandler示例详解

    前言 当大家使用mybatis作为持久层框架时,在存储和查询数据时,只需要在mapper.xml文件中配置好对应字段的JdbcType和JavaType,mybatis就可以帮我们转化对应的类型.这背后是有mybatis内置的类型转换器做转换(可见源码TypeHandlerRegistry).但是有时候,我们会对某些字段做特殊处理,比如加密和解密.状态转换.类型转换等.这个时候我们需要自定义类型转换器. 类架构 从上面的图中可以看出MyBatis中整个类型处理器实现架构,TypeHandler接

  • 以SortedList为例详解Python的defaultdict对象使用自定义类型的方法

    目录 写在前面 第一种方法: 封装成函数 第二种方法: 类封装 写在前面 最近写周赛题, 逃不开的一种题型是设计数据结构, 也就是第三题, 做这种题需要的就是对语言中的容器以及常用排序查找算法的掌握, 而我只熟悉了最基本的一些方法, 做起这些题来总是超时… 为了搞定这些题, 我决定学习一下大佬们的做法, 特别是优先队列的方法维护有序容器以及有序列表等容器, 这些都在Python中封装好了, 用起来很是方便, 但是采用defaultdict的时候, 其缺省数据类型常常需要与题目给出的特定结构匹配,

  • Angular7中创建组件/自定义指令/管道的方法实例详解

    组件 使用命令创建组件 •创建组件的命令:ng generate component 组件名 •生成的组件组成: 组件名.html .组件名.ts.组件名.less.组件名.spec.ts •在组件的控制器 @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.less'] }) 手动创建组件 1.创建一个组件ts文件 2.在组件中设

  • Android自定义View的实现方法实例详解

    一.自绘控件 下面我们准备来自定义一个计数器View,这个View可以响应用户的点击事件,并自动记录一共点击了多少次.新建一个CounterView继承自View,代码如下所示: 可以看到,首先我们在CounterView的构造函数中初始化了一些数据,并给这个View的本身注册了点击事件,这样当CounterView被点击的时候,onClick()方法就会得到调用.而onClick()方法中的逻辑就更加简单了,只是对mCount这个计数器加1,然后调用invalidate()方法.通过 Andr

  • angularJS自定义directive之带参方法传递详解

    如下所示: //自定义指令 "myEmail" grgApp.directive("myEmail",function(){ return{ restrict:'AE', scope:{toDir:'@', fromName:'@', sendEmail:'&' }, templateUrl:'/htmls/main/html/custom/email.html',} }); //控制器中的方法 $scope.send=function(msg){ aler

  • Spark自定义累加器的使用实例详解

    累加器(accumulator)是Spark中提供的一种分布式的变量机制,其原理类似于mapreduce,即分布式的改变,然后聚合这些改变.累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数. 累加器简单使用 Spark内置的提供了Long和Double类型的累加器.下面是一个简单的使用示例,在这个例子中我们在过滤掉RDD中奇数的同时进行计数,最后计算剩下整数的和. val sparkConf = new SparkConf().setAppName("Test").setM

  • Android实现定时器的五种方法实例详解

    一.Timer Timer是Android直接启动定时器的类,TimerTask是一个子线程,方便处理一些比较复杂耗时的功能逻辑,经常与handler结合使用. 跟handler自身实现的定时器相比,Timer可以做一些复杂的处理,例如,需要对有大量对象的list进行排序,在TimerTask中执行不会阻塞子线程,常常与handler结合使用,在处理完复杂耗时的操作后,通过handler来更新UI界面. timer.schedule(task, delay,period); task: Time

随机推荐