Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

接口类型值和接口值问题,包括nil值问题 #11

Closed
GuoYuefei opened this issue Jul 29, 2020 · 0 comments
Closed

接口类型值和接口值问题,包括nil值问题 #11

GuoYuefei opened this issue Jul 29, 2020 · 0 comments
Labels
golang 有关golang, 颜色取自官网

Comments

@GuoYuefei
Copy link
Owner

接口类型值和接口值问题,包括nil值问题

引言

先来看一个错误的程序

const debug = true
func main() {
    var buf *bytes.Buffer
    if debug {
        buf = new(bytes.Buffer) // enable collection of output
    }
    f(buf) // NOTE: subtly incorrect!
    if debug {
        // ...use buf...
    }
}
// If out is non-nil, output will be written to it.
func f(out io.Writer) {
    // ...do something...
    if out != nil {
        out.Write([]byte("done!\n"))
    }
}

debug=true ,开启状态是没有问题的。

但当 debug=false ,运行时就会出现空指针问题,out.Write([]byte("done!\n"))

原因在于out接口其动态类型为 *bytes.Buffer, 但是其接口值为nil。 ---> 原因是: 实参未被赋值,而是这个类型的空值

而nil的类型的动态类型和值都是nil,所以,nil != out 是为 true 的。


iface and eface

可以从源码层面看下

普通接口配型有iface类型定义, 空接口为eface

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32 // copy of _type.hash. Used for type switches.
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // variable sized
}

以上为iface的定义, iface 内部维护两个指针,tab 指向一个 itab 实体, 它表示接口的类型以及赋给这个接口的实体类型。data 则指向接口具体的值,一般而言是一个指向堆内存的指针。

再来仔细看一下 itab 结构体:_type 字段描述了实体的类型,包括内存对齐方式,大小等;inter 字段则描述了接口的类型。fun 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。

这里只会列出实体类型和接口相关的方法,实体类型的其他方法并不会出现在这里。如果你学过 C++ 的话,这里可以类比虚函数的概念。

另外,你可能会觉得奇怪,为什么 fun 数组的大小为 1,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的。

再看一下 interfacetype 类型,它描述的是接口的类型:

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

可以看到,它包装了 _type 类型,_type 实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 mhdr 字段,表示接口所定义的函数列表, pkgpath 记录定义了接口的包名。

接着来看一下 eface 的源码:

type eface struct {
  _type *_type
  data  unsafe.Pointer
}

相比 ifaceeface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。

接口的动态类型和动态值

从源码里可以看到:iface包含两个字段:tab 是接口表指针,指向类型信息;data 是数据指针,则指向具体的数据。它们分别被称为动态类型动态值。而接口值包括动态类型动态值

【引申1】接口类型和 nil 作比较 (引言中的程序错误的原因)

接口值的零值是指动态类型动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil

例子:

package main
import "fmt"
type Coder interface {
    code()
}
type Gopher struct {
    name string
}
func (g Gopher) code() {
    fmt.Printf("%s is coding\n", g.name)
}
func main() {
    var c Coder
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)
    var g *Gopher
    fmt.Println(g == nil)
    c = g
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)
}

输出为:

true
c: <nil>, <nil>
true
false
c: *main.Gopher, <nil>

【引申2】来看一个例子,看一下它的输出:

package main
import "fmt"
type MyError struct {}
func (i MyError) Error() string {
    return "MyError"
}
func main() {
    err := Process()
    fmt.Println(err)
    fmt.Println(err == nil)
}
func Process() error {
    var err *MyError = nil
    return err
}

函数运行结果:

<nil>
false

这里先定义了一个 MyError 结构体,实现了 Error 函数,也就实现了 error 接口。Process 函数返回了一个 error 接口,这块隐含了类型转换。所以,虽然它的值是 nil,其实它的类型是 *MyError,最后和 nil 比较的时候,结果为 false所以返回错误要返回无错误时,应该直接返回nil,而非用先声明再返回的方式

【引申3】如何打印出接口的动态类型和值?

直接看代码:

package main
import (
    "unsafe"
    "fmt"
)
type iface struct {
    itab, data uintptr
}
func main() {
    var a interface{} = nil
    var b interface{} = (*int)(nil)
    x := 5
    var c interface{} = (*int)(&x)
    ia := *(*iface)(unsafe.Pointer(&a))
    ib := *(*iface)(unsafe.Pointer(&b))
    ic := *(*iface)(unsafe.Pointer(&c))
    fmt.Println(ia, ib, ic)
    fmt.Println(*(*int)(unsafe.Pointer(ic.data)))
}

代码里直接定义了一个 iface 结构体,用两个指针来描述 itabdata,之后将 a, b, c 在内存中的内容强制解释成我们自定义的 iface。最后就可以打印出动态类型和动态值的地址。

Output:

{0 0} {17426912 0} {17426912 842350714568}
5

a 的动态类型和动态值的地址均为 0,也就是 nil;b 的动态类型和 c 的动态类型一致,都是 *int;最后,c 的动态值为 5。

以上自定义inface强转的方法, 在以下参考资料的《Go 语言问题集》的‘标准库’的‘unsafe’部分有讲。是本好书,值得反复看。

参考

《Go 语言问题集》

《Go语言圣经中文版(简体)》

@GuoYuefei GuoYuefei added the golang 有关golang, 颜色取自官网 label Jul 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
golang 有关golang, 颜色取自官网
Projects
None yet
Development

No branches or pull requests

1 participant