go为什么调用nil pointer的方法不会报错?

一个结构体, 具有指针型方法, 但是这个结构体的指针是nil, 调用结构体型方法报错, 调用指针型方法不会报错.
较好的回答,参考: https://groups.google.com/g/golang-nuts/c/wcrZ3P1zeAk/m/WI88iQgFMvwJ?pli=1

I think the question is really "but how would you inspect the object pointer to find and dispatch the function, as in C++ vtable." We might answer this from that angle:
Method dispatch, as is used in some other languages with something like objectpointer->methodName() involves inspection and indirection via the indicated pointer (or implicitly taking a pointer to the object) to determine what function to invoke using the named method. Some of these languages use pointer dereferencing syntax to access the named function. In any such cases, calling a member function or method on a zero pointer understandably has no valid meaning.
In Go, however, the function to be called by the Expression.Name() syntax is entirely determined by the type of Expression and not by the particular run-time value of that expression, including nil. In this manner, the invocation of a method on a nil pointer of a specific type has a clear and logical meaning. Those familiar with vtable[] implementations will find this odd at first, yet, when thinking of methods this way, it is even simpler and makes sense. Think of:
func (p *Sometype) Somemethod (firstArg int) {}
as having the literal meaning:
func SometypeSomemethod(p *Sometype, firstArg int) {}
and in this view, the body of SometypeSomemethod() is certainly free to test it's (actual) first argument (p *Sometype) for a value of nil. Note though that the calling site invoking on a nil value must have a context of the expected type. An effort to invoke an unadorned nil.Somemethod would not work in Go because there is be no implicit "Sometype" for the typeless value nil to expand the Somemethod() call into "SometypeSomemethod()"

方法值:
methodV := someStruct.SomeMethod
那么这方法就和这个接收器绑定了,可以传递给其他,帮助修改绑定的接收器的内部状态.

方法表达式:
methodExpr := SomeStuct.SomeMethod
可以参考: https://www.cnblogs.com/phpper/p/12370086.html

方法值和方法表达式, 都可以有结构体和指针型.且结构体型都会复制结构体, 结构体型的方法值每次调用都会复制一次, 且是基于创建方法值的时候, struct的值进行复制的,就像是一次胚胎冷冻.

sFunc4()
fmt.Println("invoke another time: \n")
sFunc4() // 这两次调用都会复制一次结构体,其内存地址不同

关于 nil inteface(接口零值)

https://go.dev/tour/methods/12

另外在这个仓库里 : https://github.com/ksimka/go-is-not-good, 进一步吐槽了这个设计: nil pointer are not entirely nil. 但是对应的网页内容丢失了
可以看: https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/#nil-interface-values
(给了不错的example)
也可以参考: https://codefibershq.com/blog/golang-why-nil-is-not-always-nil (给了解决方)

为防止内容再次丢失, 直接复制过来:

Why Golang Nil Is Not Always Nil? Nil Explained

image

Newcoming developers starting their adventure with Golang are suprised by differencies that Golang has. One of them is the nil value...

Many times developers writing Golang code uses error value check whether the error was raised. It could look like this:


func do() error {
    return nil
}

func main(){
    if do() != nil {
        return err
    }
}

These constructs are idiomatic to Golang, as it encourages developers to explicitly return errors as values and handle them as usual variable. But this post is not about errors, its about nil values.

Nil represents a zero value in Golang.

As you may already know, zero values are the "default" value for defined variables in Go. This means that some types doesn't cannot hold empty values, instead acquiring zero values upon initialization to get rid of the problem of checking values for emptyness. This rule translates into behaviour descirbed below, defining a variable initializes it with empty value.


var i int // 0
var t bool // false

type T struct {
    v string
}

var t T // {""}

This behaviour applies to all types in Go besides pointers, slices, maps, channels, functions, interfaces.


var s []int             // s == nil -> true
var m map[string]string // m == nil -> true
var p *int              // p == nil -> true
var c chan int          // c == nil -> true
var f func()            // f == nil -> true

type T struct {
    p *int
}
var t T             // t.p == nil -> true

var i interface{}   // i == nil -> true

var a = nil         // compile error: use of untyped nil

So far so good, there is no confusion here. Just to point out, I'm defining an i variable which will hold a value that implements empty interface, which is satisfied by all types in Go. I could have used any other interface with the same effect (nil upon initialization). The last example shows that nil is not a type therefore it cannot be used to be assigned to a value without a type.

The confusion comes when one want to use a construct like this:


var p *int
var i interface{}

i = p

if i != nil {
    fmt.Println("not a nil")
}

//the code outputs "not a nil"

What happened here? We can clearly see (and based on previous rules) that both p and i has value nil but the condition is evaluated into false. This is not a bug it is a legitimate Golang behaviour.

What you should know is that interface is the reason of all the mess. The structure behind Golang interfaces holds two values - type and value. The type being a conrete type behind an interface (if any is assigned) and the type's value. To illustrate let's take a look at annotated examples that will help you understand Golang interfaces.


var p *int              // (type=*int,value=nil)
var i interface{}       // (type=nil,value=nil)

if i != p {             // (type=*int,value=nil) != (type=nil,value=nil)
// to successfully compare these values, both type and value must match
    fmt.Println("not a nil")
}

//the code outputs "not a nil"

Another thing to consider is that hardcoded nil is always behaving like unassigned interface which is - (type=nil, value=nil). This can translate into another "weird" bahaviour, but once you understand the mechanics behind it you will be able to use it effectively.


var p *int              // (type=*int,value=nil)
var i interface{}       // (type=nil,value=nil)

if i != nil {           // (type=nil,value=nil) != (type=nil,value=nil)
    fmt.Println("not a nil")
}

i = p                   // assign p to i

// a hardcoded nil is always nil,nil (type,value)
if i != nil {           // (type=*int,value=nil) != (type=nil,value=nil)
    fmt.Println("not a nil")
}

//the code outputs "not a nil" only once

The above example is the most confusing and may lead unexpected program behaviour since i variable can be passed along to another function which takes interface{} as input type parameter, therefore checking it only for basic i == nil will not suffice. What is the fix for that?

There are two solutions, one is to compare the value with typed nil value and the second one is using reflection package. Take a look at the example:


import "reflect"
import "unsafe"

func do(v interface{}){

    if v == nil {
        // this may not always work
        // if the value is (type=*int,value=nil)
        // the condition will not trigger
        // because hardcoded `nil` is (type=nil,value=nil)
    }

    if v == (*int)(nil){
        // one solution is to compare this value
        // with concrete type that has zero (nil) value
        // but this may lead to code duplication
        // and very hard to read logic if you want
        // to check multiple cases
    }

    if reflect.ValueOf(v).IsNil() {
        // https://golang.org/pkg/reflect/#Value.IsNil
        // by using `reflect` package we can check
        // whether a value is nil but only if it is one
        // of those 5 types that can be nil.
        // So be careful using it (it panics)
    }

    if (*[2]uintptr)(unsafe.Pointer(&v))[1] == 0 {
        // there is also more unsafe way
        // it checks the value part of an interface
        // directly for zero
        // it also doesn't panic if the value
        // is not one of 5 `nil able` types
    }

}

Summing this up, the most confusing part is where you are trying to check whether the interface holds empty value (pointer, slice, function, interface, channel, map). In that case you must remember that you cannot safely rely on v == nil comparison. On the other hand why is it that you can safely compare those types against handwritten nil like the example below and get accurate results?


var i *int
fmt.Println(i == nil) // prints `true`

In the example above the compiler does not the conrete type and can help you type the written nil so basically this translates to compiler doing i == (*int)(nil). With interface the compiler is not sure about the underlying type because it can change any time (you can assign string and then pointer to the same interface variable) leading to really unexpected program behaviour.

I hope I've explained all the mystics about nil value in Golang and now you know about every edge corner that you should cover in your project.

Benchmarks

Just for the closure, here are three benchmarks that shows the performance impact when using different nil checking strategies, I've used simple counter incrementation to be sure that the condition is actually done by the runtime (the compiler may cut the if statement if it cannot prove it has any effect on the program behaviour)


package main

import (
    "reflect"
    "testing"
    "unsafe"
)

func BenchmarkSimple(b *testing.B) {
    a := 0
    for i := 0; i <= b.N; i++ {
        var v interface{}
        v = (*int)(nil)
        if v == nil {
            a++
        } else {
            a++
        }
    }
}
func BenchmarkNilPointer(b *testing.B) {
    a := 0
    for i := 0; i <= b.N; i++ {
        var v interface{}
        v = (*int)(nil)
        if v == (*int)(nil) {
            a++
        } else {
            a++
        }
    }
}
func BenchmarkReflect(b *testing.B) {
    a := 0
    for i := 0; i <= b.N; i++ {
        var v interface{}
        v = (*int)(nil)
        if v == reflect.ValueOf(v).IsNil() {
            a++
        } else {
            a++
        }
    }
}
func BenchmarkUnsafe(b *testing.B) {
    a := 0
    for i := 0; i <= b.N; i++ {
        var v interface{}
        v = (*[2]uintptr)(unsafe.Pointer(&v))[1]
        if v == (*int)(nil) {
            a++
        } else {
            a++
        }
    }
}

The results:


goos: linux
goarch: amd64
BenchmarkSimple         1000000000           0.270 ns/op
BenchmarkNilPointer     1000000000           0.277 ns/op
BenchmarkReflect        484543268            2.44 ns/op
BenchmarkUnsafe         1000000000           1.06 ns/op

As can we see there is about 10 times bigger latency when comparing to nil using reflect package and about 3 times when it comes to comparison using unsafe package.

From my experience the above nil checking gotchas doesn't occur very often because mostly we are dealing with empty interfaces in our code. The most common case might appear when developer are trying to return error values of their kind from a function that does not get assigned a value (only initialized). Take a look at a last example.


// example #1
type myerr string

func (err myerr) Error() string {
    return "an error ocurred: " + err
}

func do() error {
    var err *myerr
    //do logic...
    return err // might return nil pointer
}

func main() {
    err := do()
    print(err == nil) // prints `false` because it is nil pointer
}

// example #2
type myerr string

func (err myerr) Error() string {
    return "an error ocurred"
}

func do() *myerr {
    return nil // returns nil pointer
}

func wrap() error {
    return do() // the information about nil pointer is dissolved
}

func main() {
    err := wrap()
    print(err == nil) //prints `false` because underneath is nil pointer not empty interface
}

The solution is simple - always use error interface when returning an error from function and never initialize an empty error variable that might be return from function (as nil).This makes the code more readable and generic but also avoid the situations above.

Thank you for your time spent reading the article and I hope it resolved some of the issues about nil checking in Golang.

Code Fibers provides services for Golang development and consulting, I invite you to visit our website and contact also get to know how we work and what is our exprience

Built with ConvertKit

These posts might be interesting for you:

  1. Why appending to slice in Golang is dangerous? Common Slice Gotchas
image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容