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

目录
  • 一 前序
  • 二 事出有因
    • errors.As 方法签名
  • 三 结构体与实例的数据结构
    • 1. 结构体类型
    • 2. 实例
    • 3 方法调用
      • 3.1 方法表达式
      • 3.2 值实例调用所有方法
      • 3.3 指针实例调用所有方法
      • 3.4 空指针无法调用值方法
  • 四 接口
    • 1 接口数据结构
    • 2 接口赋值
    • 值方法集
    • 指针方法集
  • 总结

一 前序

很多时候我们以为自己懂了,但内心深处却偶有困惑,知识是严谨的,偶有困惑就是不懂,很幸运通过大量代码的磨练,终于看清困惑,并弄懂了。

本篇包括结构体,类型, 及 接口相关知识,希望对大家有所启发。

二 事出有因

搞golang也有三四个年头了,大小项目不少,各种golang书籍资料也阅无数,今天突然被一个报错搞懵了。演示代码如下:

type MyErr struct{}
func(this MyErr)Error()string{
    return "myerr"
}
func main(){
    var Err *MyErr
    errors.As(MyErr{},Err) //这一句
}

errors.As是标准库里的判断错误类型的一个简单函数,按照如上写法他运行报错,报错内容如下:

panic: errors: target must be a non-nil pointer
goroutine 1 [running]:
errors.As({0x107e280, 0x11523f8}, {0x104f3e0, 0x11523f8})
        D:/GO/src/errors/wrap.go:84 +0x3e5
github.com/pkg/errors.As(...)
        D:/GO/gopath/pkg/mod/github.com/pkg/errors@v0.9.1/go113.go:31
main.main()
        H:/information/demo1/main.go:19 +0x31
exit status 2

errors.As 方法签名

func As(err error, target interface{}) bool

起初我没有太关心报错结果,我第一感觉是指针类型实现接口有问题,于是又改实现方法,又折腾变量,有时候ide提示方法未实现,有时候运行报错,偶有成功,为啥成功我也不知道。

突然我发现我对接口一直都停留在会用的基础上,所有结构体方法接受者都用指针,所有结构体实例都用指针,一方面保证接口方法都能实现,另一方面减少对象拷贝,减少内存用量。

于是带着这个问题开始了刨根问题。在查阅资料中又发现了新的问题。

  • 指针方法集包括结构体所有方法,值方法集不包括指针方法集,为啥一个指针或者一个值实例可以调用所有方法。方法集的本质是啥?
type T struct{}
func (t T) Get() {
	fmt.Println("this is Get")
}
func (t *T) Set() {
	fmt.Println("this is set")
}
func main() {
	var a T
	a.Set()
	a.Get()
	(&a).Get()
	(&a).Set()
}
  • 为啥有时候指针对象无法调用非指针方法?如开始的err例子。
  • 嵌入类型的结构体,面对指针和值实例,方法集规律是啥?
  • 接口到底是啥?nil又是啥?
  • 结构体体结构到底是怎么样的?
  • 实例结构又如何?怎么通过实例找到相应的方法?
  • 。。。

三 结构体与实例的数据结构

1. 结构体类型

结构体就是一个模板,用于生成实例用的,包括最基本的属性集,值的方法集,指针方法集。

type T struct{
    Num int
}
func (t T) Get() int{
	fmt.Println("this is Get")
        return t.Num
}
func (t *T) Set(i int) {
	fmt.Println("this is set")
        t.Num = i
}

这就是一个定义的结构体。

func (t T) Get() 该方法的接受者 t是一个实例值,所以该方法称为值方法。

func (t *T) Set() 该方法的接受者 t 是一个指针,所以该方法成为指针方法。

2. 实例

实例就是结构体实例化后的变量,用T类型说明。

    var a T
    var b *T
    var c = T{1}
    var d = &T{1}

这四种实例定义发生了什么?数据结构如何?

实例数据结构主要包括三部分。

  • 头部信息,说明实例大小,实例是指针还是非指针等
  • 值,指针时候是指向实例的地址,非指针时候是具体的属性值
  • 类型

实例a是一个空结构体实例,其特点是a虽然没有显示赋值,但是会默认创建一个a实例,其中的属性都是"类型零值"。

实例b是一个指针类型,特点是没有被初始化,指针未任何实例。

实例c是一个显示赋值的实例,和a区别就是Num初始化值不再是"类型零值",而是1。

实例d就有点复杂了,他会有个实例及指针两种数据,指针指向实例。实例初始化非"类型零值"。

关于图中地址的说明,所有数据结构最终都是内存中的一段连续代码,都有开始地址,其他需要使用该数据的地方都是通过该地址找到这段内存信息的。当然要说到代码,内存,虚拟地址,堆栈,程序运行,会有很多内容,这里只要知道通过地址能找到该数据信息即可。

注意,上图也仅仅只是示意图,帮助理解。其中类型指针实现并不是一个真的指针而是一个关于类型元信息的偏移地址。

3 方法调用

结合上面的图,说一下方法调用问题。为啥值方法和指针方法都可以调用所有方法,并且都能成功,并且修改都可以成功。

	a.Get()
	a.Set(2)
	// b.Get() 编译器通过 运行不通过
	b.Set(3)
	c.Get()
	c.Set(4)
	d.Get()
	d.Set(5)

3.1 方法表达式

实例的方法调用的本质是函数,类似python,编译器调用该函数时候默认的第一个参数是实例值或者实例指针。

T.Get(a)

(*T).Set(b,2)

通过类型直接调用类型中的函数,这就是方法表达式调用。真实的实例调用,也是通过找到类型并调用类型的方法。关于"方法表达式"这个词出自《go语言核心编程》第三章,类型系统,有兴趣的可以看看。

方法表达式有个特点,就是不会被自动转换,通过方法表达式可以清楚知道值方法集或指针方法集是否有该方法。

在没有说到接口之前,判断一个方法是否属于方法集用这个方法表达式是比较方便的。

3.2 值实例调用所有方法

a和c本质是一样的,只是初始值不一样。拿c做例子进行讲解。

c.Get() == T.Get(a)

上边代码这个不用解释太多,就是c实例通过类型信息找到相关的值方法进行调用。

c.Set() == (&c).Set(4) == (*T).Set(c,4)

上边代码 c中对应的T中方法并不包含Set方法。

T.Set() 你会发现编译器会报错 T中没有Set方法

但*T中有方法Set,这时候编译器会生成一个*c,指针对象,在通过该对象调用Set方法。虽然通过指针对象调用Set但确实把c对象中的Num修改成功了,因为指针指向的正是c实例。如下图:

这就是为啥实例方法集中没有Set方法,也可以调用Set方法,编译器进行了自动转换,而这样设计是合理的,通过Set操作,c实例中的Num确实变成4,符合预期。

3.3 指针实例调用所有方法

b和d都是指针实例,看看上图关于b和d的数据结构示意图,这两个图里最大的区别就是有没有匿名实例,b因为是空指针没有指向任何实例,所以只有类型信息。

编译器知道你是个指针,查看类型中的所有方法,包括值方法和指针方法,有Set和Get所以编译通过,但是在运行的时候,因为是空指针,无法找到值的方法Get,所以运行时候报错 panic: errors: target must be a non-nil pointer

d因为指向一个实例,所以顺着这个实例找到Get方法进行调用,这都是编译器自动进行的。

d.Get() == (&d).Get() == (T).Get(*d)

通用使用方法表达式,也可以知道指针方法集中是没有Get方法的。

(*T).Get() 编译器不会通过 说明指针方法集中确实没有Get函数 所以只能通过转化成实例来调动Get方法

这种自动转化及操作的结果也是符合预期的,拿到了d指针指向的实例的数据。

3.4 空指针无法调用值方法

在回过头看最初的err问题,原因就出在给了一个空指针,要通过一个空指针找到一个值方法,但是运行时候无法找到,所以panic了

四 接口

正常情况下,值实例还是指针实例都可以调用所有方法,并且修改都可以成功,那为什么要区分值的方法集和指针的方法集,这就不得不提接口。

方法集是给接口准备。

方法集是"符合预期"的。

可以说因为接口的需要才会有方法集概念,只有接口中的方法与方法集中的方法相匹配时候,该方法集的实例才是该接口的实现实例。

可是问题又来了,明明一个实例对象不管是指针还是非指针实例都可以执行全部的方法,技术上完全可以实现,为什么还要区分指针非指针方法?这是因为"不符合预期",为什么,为什么"不符合预期",看下边解释。

1 接口数据结构

要说明白接口和方法集的关系不是一件容易的事,先从接口结构说起。

接口类型跟struct类型不同,字面上看,接口只有方法头,没有属性。

接口实例跟一般的struct实例也不一样,它是一种动态的实例,只有接口实例被具体实例(值或指针)赋值的时候,接口实例才能确定。如下图。

接口实例跟结构体实例类似,也包括两部分,值和类型。

接口中的值是动态的,当被具体结构体实例赋值时候才能确定该值。该值就是结构体实例的值的拷贝,当实例是非指针时候会把数据都拷贝过来,当是实例是指针时候会把指针拷贝过来。golang中一切赋值都是拷贝,包括接口赋值,也是因为拷贝才会有很多"不符合预期的"结果。

接口中的类型包括动态类型和自身的接口类型,自身类型没啥好说的,看上图就明白了,主要是动态类型,这个是存储了当前赋值的结构体实例的类型。

2 接口赋值

以下面的接口赋值代码进行说明解释。

package main
type I interface {
	Get() int
	Set(i int)
}
type T struct {
	Num int
}
func (t T) Get() int {
	return t.Num
}
func (t *T) Set(num int) {
	t.Num = num
}
func main() {
	var a T
	var b *T
	var c = T{}
	var d = &T{}
	var ia I = a //编译不通过 方法集不匹配
	var ib I = b //编译通过 运行会报错 panic: runtime error: invalid memory address or nil pointer dereference
	var ic I = c //编译不通过 方法集不匹配
	var id I = d
}

例子代码很简单,就是一个接口类型I,一个struct类型T,其实现了值Get方法,指针Set方法。

上边代码中a,b,c,d已经在上部分进行过讲解了。

ia,ib,ic,id赋值过程如下图:

值方法集

ia,ic接口对象其实在编辑阶段IDE就会给出报错提示,实例和接口不匹配,因为a和c实例方法集中只有一个Get函数,可以通过前边提到的"表达式方法"进行验证,这里通过IDE提示也知道缺少Set函数。

那么问题来了,在第一部分单独a,c对象是可以调用所有方法,这里接口实现为啥要弄出个方法集进行限制?因为"拷贝"和"不符合预期"。

假设a,c可以成功赋值给接口ia,ic,赋值后a,c中的数据会拷贝到接口的动态值区域,要是成功执行了Set函数,将接口动态值区域的数据进行了修改,那原来的a,c中的数据并未改变,这个是"不符合预期的"。所以干脆就不允许这么操作。

更常用的"不符合预期"解释代码是当接口是参数值时候。如下代码。

func DoT(a I) {
	a.Set(11)
}
func main(){
    ...
    DoT(ic)
    fmt.Println(ic.Get())
}

DoT函数用I做参数,内部对I进行了操作,用ic或者ia做参数,如果可以成功,最后打印ic或者ia中的值,并未改变,这不符合预期,很令人困惑。这段原理可参考<<go核心编程>>第三章类型系统相关描述。

指针方法集

ib和id都是指针类型,其方法集包括所有方法,即Get和Set,其中Get是通过编译器自动转化进行间接调用,值实例不允许调用指针实例的方法集是因为"不符合预期",那指针实例就允许调用值实例的方法了?是的,允许,因为"符合预期"。

还用下面的代码做解释。

func DoT(a I) {
    a.Set(a.Get()++)
}
func main(){
    ...
    DoT(id)
    fmt.Println(id.Get())
}

这里用id做参数,最终执行完,结果id确实增加了1,符合预期。

结合前边接口赋值的图进行分析,接口动态值区域拷贝了一份id的指针值,这个指针指向一个具体的实例。如下图。

从这里可以看出对id的任何操作其实都是对具体的实例进行的操作,所以无论读写都是符合预期的,所以当使用指针调用Get方法时候就会进行自动转化调用值的Get方法。

至于ib为啥编译通过,运行时候就报错,也是因为指针是个nil值,无法自动转化找到Get方法。

总结

翻了好几天资料,本来想把嵌入类型和反射都写进来,但是时间有点仓促,大家可以结合上边的讲解,自行对嵌入类型和反射进行研究,基础原理都一样。

这里总结一下:

实例都包括两部分,值和类型,编译器正是通过实例类型所以才知道了其方法集。

单独实例使用时候,是允许调用所有方法的,调用非自身方法集时候编译器会自动进行转换,并且都会调用成功,符合预期。

实例赋值给接口时候,是把实例信息拷贝到接口中的,其数据结构和原来实例完全不一样了,同时接口会严格检查方法集,以防止不符合预期行为发生。

实例是指针时候,并且为空的时候,并且包含非指针方法时候,无论是该实例的接口还是该实例,都不能进行任何方法调用,否则会有运行时panic发生。未指向任何具体数据变量,无论读写肯定报错。

接口断言知道为啥一定要是接口才能进行断言吧,因为接口的动态值和动态类型要进行动态填充,接口断言也可以判断一个实例的方法集,而且是安全的判断

_,ok:=interface{}(a).(I)

判断一个实例是否有哪个方法,方法集中的方法有哪些,目前看可以通过三种方法"方法表达式"","接口赋值","接口断言"。

其实还有好多知识点比如nil类型,空接口,空指针,相互比较时候真假结果,嵌入结构体方法集,反射操作,等等,只要把原理搞清了都很容易理解的。

以上就是goalng 结构体 方法集 接口实例详解的详细内容,更多关于goalng 结构体 方法集 接口的资料请关注我们其它相关文章!

(0)

相关推荐

  • golang gorm模型结构体的定义示例

    目录 1. 模型 1.1. 模型定义 2. 约定 2.1. gorm.Model 结构体 2.2. 表名是结构体名称的复数形式 2.3. 更改默认表名 2.4. 列名是字段名的蛇形小写 2.5. 字段ID为主键 2.6. 字段CreatedAt用于存储记录的创建时间 2.7. 字段UpdatedAt用于存储记录的修改时间 2.8. 字段DeletedAt用于存储记录的删除时间,如果字段存在 1. 模型 1.1. 模型定义 type User struct { gorm.Model Birthda

  • go语言结构体指针操作示例详解

    目录 指针 go指针操作 不能操作不合法指向 new函数 指针做函数的参数 数组指针 结构体指针变量 结构体成员普通变量 结构体成员指针变量 结构体比较和赋值 结构体作为函数参数 指针 指针是代表某个内存地址的值.内存地址储存另一个变量的值. 指针(地址),一旦定义了不可改变,指针指向的值可以改变 go指针操作 1.默认值nil,没有NULL常量 2.操作符“&”取变量地址,“*“通过指针(地址)访问目标对象(指向值) 3.不支持指针运算,不支持“->”(箭头)运算符,直接用“.”访问目标成

  • Go Ginrest实现一个RESTful接口

    目录 背景 特性 使用例子 实现原理 功能列表 处理请求 处理响应 处理错误 请求上下文操作 请求结构体处理 注 背景 基于现在微服务或者服务化的思想,我们大部分的业务逻辑处理函数都是长这样的: 比如grpc服务端: func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq) (*pb.GetUserInfoRsp, error) { // 业务逻辑 // ... } grpc客户端: func (s *S

  • go swagger生成接口文档使用教程

    目录 前言 Swagger介绍 1.安装 2.检测是否安装成功 3.安装gin-swagger扩展 使用 1.添加注释 2.生成接口文档数据 3.引入gin-swagger渲染文档数据 总结 前言 这篇文章主要介绍了Go语言使用swagger生成接口文档的方法,希望能够对大家的学习或工作具有一定的帮助,需要的朋友可以参考下. 在前后端分离的项目开发过程中,如果后端同学能够提供一份清晰明了的接口文档,那么就能极大地提高大家的沟通效率和开发效率.那如何维护接口文档,历来都是令人头痛的,感觉很浪费精力

  • Go语言fsnotify接口实现监测文件修改

    目录 前言 安装工具 关键类型 Event结构体 Op类型 Watcher结构体 Channel 函数 Watcher工厂函数 完整例子 前言 在开发过程中,经常需要观察本地文件系统的更改.经过谷歌了几个小时后,到了一个简单的工具来做这件事. 该工具就是fsnotify是一个Go跨平台文件系统通知工具.它提供了一个简单的接口来监测本地文件系统中的更改. 本文我们就来看看如何使用这个工具. 安装工具 $ go get github.com/fsnotify/fsnotify 关键类型 我们先来了解

  • 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).本质上,一个方法则是一个和特殊类型关联的函数. 一个面向对象的程序会用方法来表达其属性

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

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

  • 基于C#调用c++Dll结构体数组指针的问题详解

    C#调用c++dll文件是一件很麻烦的事情,首先面临的是数据类型转换的问题,相信经常做c#开发的都和我一样把学校的那点c++底子都忘光了吧(语言特性类). 网上有一大堆得转换对应表,也有一大堆的转换实例,但是都没有强调一个更重要的问题,就是c#数据类型和c++数据类型占内存长度的对应关系. 如果dll文件中只包含一些基础类型,那这个问题可能可以被忽略,但是如果是组合类型(这个叫法也许不妥),如结构体.类类型等,在其中的成员变量的长度的申明正确与否将决定你对dll文件调用的成败. 如有以下代码,其

  • C#中的只读结构体(readonly struct)详解

    翻译自 John Demetriou 2018年4月8日 的文章 <C# 7.2 – Let's Talk About Readonly Structs>[1] 在本文中,我们来聊一聊从 C# 7.2 开始出现的一个特性 readonly struct. 任一结构体都可以有公共属性.私有属性访问器等等.我们从以下结构体示例来开始讨论: public struct Person { public string Name { get; set; } public string Surname {

  • Properties 持久的属性集的实例详解

    Properties 持久的属性集的实例详解 特点: 1.Hashtable的子类,map集合中的方法都可以用. 2.该集合没有泛型.键值都是字符串. 3.它是一个可以持久化的属性集.键值可以存储到集合中,也可以存储到持久化的设备(硬盘.U盘.光盘)上.键值的来源也可以是持久化的设备. // 根据key读取value public void readValue(String filePath, String key) { Properties props = new Properties();

  • java中的interface接口实例详解

     java中的interface接口实例详解 接口:Java接口是一些方法表征的集合,但是却不会在接口里实现具体的方法. java接口的特点如下: 1.java接口不能被实例化 2.java接口中声明的成员自动被设置为public,所以不存在private成员 3.java接口中不能出现方法的具体实现. 4.实现某个接口就必须要实现里面定义的所有方法. 接下来看一个实现接口的案例: package hello;   interface competer{ //定义接口 void set_comp

  • Java函数式接口Supplier接口实例详解

    这篇文章主要介绍了Java函数式接口Supplier接口实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供. 下面是最简单的Supplier接口及使用示例. Supplier接口概述 // Supplier接口源码 @FunctionalInterface public interface Supplier<T>

  • C语言中的自定义类型之结构体与枚举和联合详解

    目录 1.结构体 1.1结构的基础知识 1.2结构的声明 1.3特殊的声明 1.4结构的自引用 1.5结构体变量的定义和初始化 1.6结构体内存对齐 1.7修改默认对齐数 1.8结构体传参 2.位段 2.1什么是位段 2.2位段的内存分配 2.3位段的跨平台问题 2.4位段的应用 3.枚举 3.1枚举类型的定义 3.2枚举的优点 3.3枚举的使用 4.联合 4.1联合类型的定义 4.2联合的特点 4.3联合大小的计算 1.结构体 1.1结构的基础知识 结构是一些值的集合,这些值称为成员变量.结构

  • Golang中结构体映射mapstructure库深入详解

    目录 mapstructure库 字段标签 内嵌结构 未映射字段 Metadata 弱类型输入 逆向转换 解码器 示例 在数据传递时,需要先编解码:常用的方式是JSON编解码(参见<golang之JSON处理>).但有时却需要读取部分字段后,才能知道具体类型,此时就可借助mapstructure库了. mapstructure库 mapstructure可方便地实现map[string]interface{}与struct间的转换:使用前,需要先导入库: go get github.com/m

  • Java使用agent实现main方法之前的实例详解

    Java使用agent实现main方法之前的实例详解 创建Agent项目 PreMainExecutor 类,在main方法之前执行此方法 public class PreMainExecutor { public static void premain(String agentOps, Instrumentation inst){ System.out.println("premain execute.........."); } } META-INF/MANIFEST.MF Man

  • zookeeper python接口实例详解

    本文主要讲python支持zookeeper的接口库安装和使用.zk的python接口库有zkpython,还有kazoo,下面是zkpython,是基于zk的C库的python接口. zkpython安装 前提是zookeeper安装包已经在/usr/local/zookeeper下 cd /usr/local/zookeeper/src/c ./configure make make install wget --no-check-certificate http://pypi.python

随机推荐