深入了解Golang的指針用法
與C語(yǔ)言一樣,Go語(yǔ)言中同樣有指針,通過指針,我們可以只傳遞變量的內(nèi)存地址,而不是傳遞整個(gè)變量,這在一定程度上可以節(jié)省內(nèi)存的占用,但凡事有利有弊,Go指針在使用也有一些注意點(diǎn),稍不留神就會(huì)踩坑,下面就讓我們一起來細(xì)嗦下。
1.指針類型的變量
在Golang中,我們可以通過**取地址符號(hào)&**得到變量的地址,而這個(gè)新的變量就是一個(gè)指針類型的變量,指針變量與普通變量的區(qū)別在于,它存的是內(nèi)存地址,而不是實(shí)際的值。
圖一
如果是普通類型的指針變量(比如 int
),是無法直接對(duì)其賦值的,必須通過 * 取值符號(hào)
才行。
func main() { num := 1 numP := &num //numP = 2 // 報(bào)錯(cuò):(type untyped int) cannot be represented by the type *int *numP = 2 }
但結(jié)構(gòu)體卻比較特殊,在日常開發(fā)中,我們經(jīng)常看到一個(gè)結(jié)構(gòu)體指針的內(nèi)部變量仍然可以被賦值,比如下面這個(gè)例子,這是為什么呢?
type Test struct { Num int } // 直接賦值和指針賦值 func main() { test := Test{Num: 1} test.Num = 3 fmt.Println("v1", test) // 3 testP := &test testP.Num = 4 // 結(jié)構(gòu)體指針可以賦值 fmt.Println("v2", test) // 4 }
這是因?yàn)榻Y(jié)構(gòu)體本身是一個(gè)連續(xù)的內(nèi)存,通過 testP.Num
,本質(zhì)上拿到的是一個(gè)普通變量,并不是一個(gè)指針變量,所以可以直接賦值。
圖二
那slice、map、channel這些又該怎么理解呢?為什么不用取地址符號(hào)也能打印它們的地址?比如下面的例子
func main() { nums := []int{1, 2, 3} fmt.Printf("%p\n", nums) // 0xc0000160c0 fmt.Printf("%p\n", &nums[0]) // 0xc0000160c0 maps := map[string]string{"aa": "bb"} fmt.Printf("%p\n", maps) // 0xc000076180 ch := make(chan int, 0) fmt.Printf("%p\n", ch) // 0xc00006c060 }
這是因?yàn)椋?strong>它們本身就是指針類型!只不過Go內(nèi)部為了書寫的方便,并沒有要求我們?cè)谇懊婕由?*** 符號(hào)**。
在Golang的運(yùn)行時(shí)內(nèi)部,創(chuàng)建slice的時(shí)候其實(shí)返回的就是一個(gè)指針:
// 源碼 runtime/slice.go // 返回值是:unsafe.Pointer func makeslice(et *_type, len, cap int) unsafe.Pointer { mem, overflow := math.MulUintptr(et.size, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap { // NOTE: Produce a 'len out of range' error instead of a // 'cap out of range' error when someone does make([]T, bignumber). // 'cap out of range' is true too, but since the cap is only being // supplied implicitly, saying len is clearer. // See golang.org/issue/4085. mem, overflow := math.MulUintptr(et.size, uintptr(len)) if overflow || mem > maxAlloc || len < 0 { panicmakeslicelen() } panicmakeslicecap() } return mallocgc(mem, et, true) }
而且返回的指針地址其實(shí)就是slice第一個(gè)元素的地址(上面的例子也體現(xiàn)了),當(dāng)然如果slice是一個(gè)nil,則返回的是 0x0
的地址。slice在參數(shù)傳遞的時(shí)候其實(shí)拷貝的指針的地址,底層數(shù)據(jù)是共用的,所以對(duì)其修改也會(huì)影響到函數(shù)外的slice,在下面也會(huì)講到。
map和slice其實(shí)也是類似的,在在Golang的運(yùn)行時(shí)內(nèi)部,創(chuàng)建map的時(shí)候其實(shí)返回的就是一個(gè)hchan指針:
// 源碼 runtime/chan.go // 返回值是:*hchan func makechan(t *chantype, size int) *hchan { elem := t.elem // compiler checks this but be safe. if elem.size >= 1<<16 { throw("makechan: invalid channel element type") } ... return c }
最后,為什么 fmt.Printf
函數(shù)能夠直接打印slice、map的地址,除了上面的原因,還有一個(gè)原因是其內(nèi)部也做了特殊處理:
// 第一層源碼 func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) } // 第二層源碼 func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintf(format, a) // 核心 n, err = w.Write(p.buf) p.free() return } // 第三層源碼 func (p *pp) doPrintf(format string, a []interface{}) { ... default: // Fast path for common case of ascii lower case simple verbs // without precision or width or argument indices. if 'a' <= c && c <= 'z' && argNum < len(a) { ... p.printArg(a[argNum], rune(c)) // 核心是這里 argNum++ i++ continue formatLoop } // Format is more complex than simple flags and a verb or is malformed. break simpleFormat } } // 第四層源碼 func (p *pp) printArg(arg interface{}, verb rune) { p.arg = arg p.value = reflect.Value{} ... case 'p': p.fmtPointer(reflect.ValueOf(arg), 'p') return } ... } // 最后了 func (p *pp) fmtPointer(value reflect.Value, verb rune) { var u uintptr switch value.Kind() { // 這里對(duì)這些特殊類型直接獲取了其地址 case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer: u = value.Pointer() default: p.badVerb(verb) return } ... }
2.Go只有值傳遞,沒有引用傳遞
值傳遞和引用傳遞相信大家都比較了解,在函數(shù)的調(diào)用過程中,如果是值傳遞,則在傳遞過程中,其實(shí)就是將參數(shù)的值復(fù)制一份傳遞到函數(shù)中,如果在函數(shù)內(nèi)對(duì)其修改,并不會(huì)影響函數(shù)外面的參數(shù)值,而引用傳遞則相反。
type User struct { Name string Age int } // 引用傳遞 func setNameV1(user *User) { user.Name = "test_v1" } // 值傳遞 func setNameV2(user User) { user.Name = "test_v2" } func main() { u := User{Name: "init"} fmt.Println("init", u) // init {init 0} up := &u setNameV1(up) fmt.Println("v1", u) // v1 {test_v1 0} setNameV2(u) fmt.Println("v2", u) // v2 {test_v1 0} }
但在Golang中,這所謂的“引用傳遞”其實(shí)本質(zhì)上是值傳遞,因?yàn)檫@時(shí)候也發(fā)生了拷貝,只不過這時(shí)拷貝的是指針,而不是變量的值,所以**“Golang的引用傳遞其實(shí)是引用的拷貝”。**
圖三
可以通過以下代碼驗(yàn)證:
type User struct { Name string Age int } // 注意這里有個(gè)誤區(qū),我一開始看 user(v1)打印后的地址和一開始(init)是一致的,從而以為這是引用傳遞 // 其實(shí)這里的user應(yīng)該看做一個(gè)指針變量,我們需要對(duì)比的是它的地址,所以還要再取一次地址 func setNameV1(user *User) { fmt.Printf("v1: %p\n", user) // 0xc0000a4018 與 init的地址一致 fmt.Printf("v1_p: %p\n", &user) // 0xc0000ac020 user.Name = "test_v1" } // 值傳遞 func setNameV2(user User) { fmt.Printf("v2_p: %p\n", &user) //0xc0000a4030 user.Name = "test_v2" } func main() { u := User{Name: "init"} up := &u fmt.Printf("init: %p \n", up) //0xc0000a4018 setNameV1(up) setNameV2(u) }
注:slice、map等本質(zhì)也是如此。
3.for range與指針
for range
是在Golang中用于遍歷元素,當(dāng)它與指針結(jié)合時(shí),稍不留神就會(huì)踩坑,這里有一段經(jīng)典代碼:
type User struct { Name string Age int } func main() { userList := []User { User{Name: "aa", Age: 1}, User{Name: "bb", Age: 1}, } var newUser []*User for _, u := range userList { newUser = append(newUser, &u) } // 第一次:bb // 第二次:bb for _, nu := range newUser { fmt.Printf("%+v", nu.Name) } }
按照正常的理解,應(yīng)該第一次輸出aa
,第二次輸出bb
,但實(shí)際上兩次都輸出了bb
,這是因?yàn)?for range
的時(shí)候,變量u實(shí)際上只初始化了一次(每次遍歷的時(shí)候u都會(huì)被重新賦值,但是地址不變),導(dǎo)致每次append的時(shí)候,添加的都是同一個(gè)內(nèi)存地址,所以最終指向的都是最后一個(gè)值bb。
我們可以通過打印指針地址來驗(yàn)證:
func main() { userList := []User { User{Name: "aa", Age: 1}, User{Name: "bb", Age: 1}, } var newUser []*User for _, u := range userList { fmt.Printf("point: %p\n", &u) fmt.Printf("val: %s\n", u.Name) newUser = append(newUser, &u) } } // 最終輸出結(jié)果如下: point: 0xc00000c030 val: aa point: 0xc00000c030 val: bb
類似的錯(cuò)誤在Goroutine
也經(jīng)常發(fā)生:
// 這里要注意下,理論上這里都應(yīng)該輸出10的,但有可能出現(xiàn)執(zhí)行到7或者其他值的時(shí)候就輸出了,所以實(shí)際上這里不完全都輸出10 func main() { for i := 0; i < 10; i++ { go func(idx *int) { fmt.Println("go: ", *idx) }(&i) } time.Sleep(5 * time.Second) }
4.閉包與指針
什么是閉包,一個(gè)函數(shù)和對(duì)其周圍狀態(tài)(lexical environment,詞法環(huán)境)的引用捆綁在一起(或者說函數(shù)被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個(gè)內(nèi)層函數(shù)中訪問到其外層函數(shù)的作用域。
當(dāng)閉包與指針進(jìn)行結(jié)合時(shí),如果閉包里面是一個(gè)指針變量,則外部變量的改變,也會(huì)影響到該閉包,起到意想不到的效果,讓我們繼續(xù)在舉幾個(gè)例子進(jìn)行說明:
func incr1(x *int) func() { return func() { *x = *x + 1 // 這里是一個(gè)指針 fmt.Printf("incr point x = %d\n", *x) } } func incr2(x int) func() { return func() { x = x + 1 fmt.Printf("incr normal x = %d\n", x) } } func main() { x := 1 i1 := incr1(&x) i2 := incr2(x) i1() // point x = 2 i2() // normal x = 2 i1() // point x = 3 i2() // normal x = 3 x = 100 i1() // point x = 101 // 閉包1的指針變量受外部影響,被重置為100,并繼續(xù)遞增 i2() // normal x = 4 i1() // point x = 102 i2() // normal x = 5 }
5.指針與內(nèi)存逃逸
內(nèi)存逃逸的場(chǎng)景有很多,這里只討論由指針引發(fā)的內(nèi)存逃逸。理想情況下,肯定是盡量減少內(nèi)存逃逸,因?yàn)檫@意味著GC(垃圾回收)的壓力會(huì)減小,程序也會(huì)運(yùn)行得更快。不過,使用指針又能減少內(nèi)存的占用,所以這本質(zhì)是內(nèi)存和GC的權(quán)衡,需要合理使用。
下面是指針引發(fā)的內(nèi)存逃逸的三種場(chǎng)景(歡迎大家補(bǔ)充~)
第一種場(chǎng)景:函數(shù)返回局部變量的指針
type Escape struct { Num1 int Str1 *string Slice []int } // 返回局部變量的指針 func NewEscape() *Escape { return &Escape{} // &Escape{} escapes to heap } func main() { e := &Escape{Num1: 0} }
第二種場(chǎng)景:被已經(jīng)逃逸的變量引用的指針
func main() { e := NewEscape() e.SetNum1(10) name := "aa" // e.Str1 中,e是已經(jīng)逃逸的變量, &name是被引用的指針 e.Str1 = &name // moved to heap: name }
第三種場(chǎng)景:被指針類型的slice、map和chan引用的指針
func main() { e := NewEscape() e.SetNum1(10) name := "aa" e.Str1 = &name // 指針類型的slice arr := make([]*int, 2) n := 10 // moved to heap: n arr[0] = &n // 被引用的指針 }
以上就是深入了解Golang的指針用法的詳細(xì)內(nèi)容,更多關(guān)于Golang指針的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang實(shí)現(xiàn)openssl自簽名雙向認(rèn)證的詳細(xì)步驟
這篇文章主要介紹了golang實(shí)現(xiàn)openssl自簽名雙向認(rèn)證的詳細(xì)步驟,本文分步驟給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-03-03golang?使用sort.slice包實(shí)現(xiàn)對(duì)象list排序
這篇文章主要介紹了golang?使用sort.slice包實(shí)現(xiàn)對(duì)象list排序,對(duì)比sort跟slice兩種排序的使用方式區(qū)別展開內(nèi)容,需要的小伙伴可以參考一下2022-03-03Go語(yǔ)言排序算法之插入排序與生成隨機(jī)數(shù)詳解
從這篇文章開始將帶領(lǐng)大家學(xué)習(xí)Go語(yǔ)言的經(jīng)典排序算法,比如插入排序、選擇排序、冒泡排序、希爾排序、歸并排序、堆排序和快排,二分搜索,外部排序和MapReduce等,本文將先詳細(xì)介紹插入排序,并給大家分享了go語(yǔ)言生成隨機(jī)數(shù)的方法,下面來一起看看吧。2017-11-11goland中使用leetcode插件實(shí)現(xiàn)
本文主要介紹了goland中使用leetcode插件實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04Go標(biāo)準(zhǔn)庫(kù)http?server優(yōu)雅啟動(dòng)深入理解
這篇文章主要介紹了Go標(biāo)準(zhǔn)庫(kù)http?server優(yōu)雅啟動(dòng)深入理解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01詳解在Go語(yǔ)言中如何實(shí)現(xiàn)枚舉類型
枚舉類型是一種常用的數(shù)據(jù)類型,用于表示一組有限的、預(yù)定義的、具名的常量值。而在?Go?語(yǔ)言里是沒有內(nèi)置枚舉類型的,所以本文將介紹如何實(shí)現(xiàn)?“枚舉類型”,需要的可以參考一下2023-04-04Golang使用JWT進(jìn)行認(rèn)證和加密的示例詳解
JWT是一個(gè)簽名的JSON對(duì)象,通常用作Oauth2的Bearer?token,JWT包括三個(gè)用.分割的部分。本文將利用JWT進(jìn)行認(rèn)證和加密,感興趣的可以了解一下2023-02-02Go?Web開發(fā)之Gin多服務(wù)配置及優(yōu)雅關(guān)閉平滑重啟實(shí)現(xiàn)方法
這篇文章主要為大家介紹了Go?Web開發(fā)之Gin多服務(wù)配置及優(yōu)雅關(guān)閉平滑重啟實(shí)現(xiàn)方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01