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閱讀 229,565評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內容