我报名参与金石计划1期应战——分割10万奖池,这是我的第1篇文章,点击查看活动概况

一 前序

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


二 事出有因

搞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提示办法未完成,有时分运转报错,偶有成功,为啥成功我也不知道。

忽然我发现我对接口一直都停留在会用的根底上,一切结构体办法接受者都用指针,一切结构体实例都用指针,一方面确保接口办法都能完成,另一方面减少目标复制,减少内存用量。

于是带着这个问题开端了刨根问题。在查阅材猜中又发现了新的问题。

  1. 指针办法集包括结构体一切办法,值办法集不包括指针办法集,为啥一个指针或者一个值实例能够调用一切办法。办法集的实质是啥?
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()
}
  1. 为啥有时分指针目标无法调用非指针办法?如开端的err比如。
  2. 嵌入类型的结构体,面临指针和值实例,办法集规律是啥?
  3. 接口到底是啥?nil又是啥?
  4. 结构体体结构到底是怎样样的?
  5. 实例结构又怎么?怎样经过实例找到相应的办法?
  6. 。。。

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

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}

这四种实例定义产生了什么?数据结构怎么?

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

  1. 头部信息,阐明实例巨细,实例是指针仍是非指针等
  2. 值,指针时分是指向实例的地址,非指针时分是详细的特点值
  3. 类型

实例a是一个空结构体实例,其特点是a虽然没有显现赋值,可是会默许创建一个a实例,其间的特点都是”类型零值”。

goalng 结构体 方法集 接口 终于捋清了

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

goalng 结构体 方法集 接口 终于捋清了

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

goalng 结构体 方法集 接口 终于捋清了

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

goalng 结构体 方法集 接口 终于捋清了

关于图中地址的阐明,一切数据结构终究都是内存中的一段连续代码,都有开端地址,其他需求运用该数据的地方都是经过该地址找到这段内存信息的。当然要提到代码,内存,虚拟地址,堆栈,程序运转,会有很多内容,这儿只要知道经过地址能找到该数据信息即可。

注意,上图也仅仅仅仅示意图,协助了解。其间类型指针完成并不是一个真的指针而是一个关于类型元信息的偏移地址。

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实例。如下图:

goalng 结构体 方法集 接口 终于捋清了

这便是为啥实例办法会集没有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实例也不相同,它是一种动态的实例,只要接口实例被详细实例(值或指针)赋值的时分,接口实例才干确认。如下图。

goalng 结构体 方法集 接口 终于捋清了

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

接口中的值是动态的,当被详细结构体实例赋值时分才干确认该值。该值便是结构体实例的值的复制,当实例是非指针时分会把数据都复制过来,当是实例是指针时分会把指针复制过来。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赋值过程如下图:

goalng 结构体 方法集 接口 终于捋清了

值办法集

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的指针值,这个指针指向一个详细的实例。如下图。

goalng 结构体 方法集 接口 终于捋清了

从这儿能够看出对id的任何操作其实都是对详细的实例进行的操作,所以不管读写都是契合预期的,所以当运用指针调用Get办法时分就会进行主动转化调用值的Get办法。

至于ib为啥编译经过,运转时分就报错,也是由于指针是个nil值,无法主动转化找到Get办法。

总结

翻了好几天材料,原本想把嵌入类型和反射都写进来,可是时刻有点匆促,我们能够结合上边的解说,自行对嵌入类型和反射进行研究,根底原理都相同。

这儿总结一下:

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

独自实例运用时分,是答应调用一切办法的,调用非本身办法集时分编译器会主动进行转换,并且都会调用成功,契合预期。

实例赋值给接口时分,是把实例信息复制到接口中的,其数据结构和原来实例彻底不相同了,同时接口会严格检查办法集,以防止不契合预期行为产生。

实例是指针时分,并且为空的时分,并且包括非指针办法时分,不管是该实例的接口仍是该实例,都不能进行任何办法调用,否则会有运转时panic产生。未指向任何详细数据变量,不管读写必定报错。

接口断语知道为啥一定要是接口才干进行断语吧,由于接口的动态值和动态类型要进行动态填充,接口断语也能够判别一个实例的办法集,并且是安全的判别


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

判别一个实例是否有哪个办法,办法会集的办法有哪些,目前看能够经过三种办法”办法表达式””,”接口赋值”,”接口断语”。

其实还有很多常识点比如nil类型,空接口,空指针,彼此比较时分真假成果,嵌入结构体办法集,反射操作,等等,只要把原理搞清了都很简单了解的。