?
?
與C語言一樣,Go語言中同樣有指針,通過指針,我們可以只傳遞變量的內(nèi)存地址,而不是傳遞整個變量,這在一定程度上可以節(jié)省內(nèi)存的占用,但凡事有利有弊,Go指針在使用也有一些注意點,稍不留神就會踩坑,下面就讓我們一起來細嗦下。
?
1.指針類型的變量
?
在Golang中,我們可以通過取地址符號&?得到變量的地址,而這個新的變量就是一個指針類型的變量,指針變量與普通變量的區(qū)別在于,它存的是內(nèi)存地址,而不是實際的值。
?

如果是普通類型的指針變量(比如 int ),是無法直接對其賦值的,必須通過?* 取值符號才行。
- ?
- ?
- ?
- ?
- ?
- ?
- ?
func main() {num := 1numP := &num//numP = 2 // 報錯:(type untyped int) cannot be represented by the type *int*numP = 2}
但結(jié)構(gòu)體卻比較特殊,在日常開發(fā)中,我們經(jīng)??吹揭粋€結(jié)構(gòu)體指針的內(nèi)部變量仍然可以被賦值,比如下面這個例子,這是為什么呢?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
type Test struct {Num int}// 直接賦值和指針賦值func main() {test := Test{Num: 1}test.Num = 3fmt.Println("v1", test) // 3testP := &testtestP.Num = 4 // 結(jié)構(gòu)體指針可以賦值fmt.Println("v2", test) // 4}
這是因為結(jié)構(gòu)體本身是一個連續(xù)的內(nèi)存,通過 testP.Num ,本質(zhì)上拿到的是一個普通變量,并不是一個指針變量,所以可以直接賦值。
?

那slice、map、channel這些又該怎么理解呢?為什么不用取地址符號也能打印它們的地址?比如下面的例子
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
func main() {nums := []int{1, 2, 3}fmt.Printf("%p ", nums) // 0xc0000160c0fmt.Printf("%p ", &nums[0]) // 0xc0000160c0maps := map[string]string{"aa": "bb"}fmt.Printf("%p ", maps) // 0xc000076180ch := make(chan int, 0)fmt.Printf("%p ", ch) // 0xc00006c060}
這是因為,它們本身就是指針類型!只不過Go內(nèi)部為了書寫的方便,并沒有要求我們在前面加上?符號。
?
在Golang的運行時內(nèi)部,創(chuàng)建slice的時候其實返回的就是一個指針:
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
// 源碼 runtime/slice.go// 返回值是:unsafe.Pointerfunc 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)}
而且返回的指針地址其實就是slice第一個元素的地址(上面的例子也體現(xiàn)了),當(dāng)然如果slice是一個nil,則返回的是?0x0?的地址。slice在參數(shù)傳遞的時候其實拷貝的指針的地址,底層數(shù)據(jù)是共用的,所以對其修改也會影響到函數(shù)外的slice,在下面也會講到。
?
map和slice其實也是類似的,在在Golang的運行時內(nèi)部,創(chuàng)建map的時候其實返回的就是一個hchan指針:
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
// 源碼 runtime/chan.go// 返回值是:*hchanfunc 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的地址,除了上面的原因,還有一個原因是其內(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 = argp.value = reflect.Value{}...case 'p':p.fmtPointer(reflect.ValueOf(arg), 'p')return}...}// 最后了func (p *pp) fmtPointer(value reflect.Value, verb rune) {var u uintptrswitch value.Kind() {// 這里對這些特殊類型直接獲取了其地址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ù)的值復(fù)制一份傳遞到函數(shù)中,如果在函數(shù)內(nèi)對其修改,并不會影響函數(shù)外面的參數(shù)值,而引用傳遞則相反。
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
type User struct {Name stringAge 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 := &usetNameV1(up)fmt.Println("v1", u) // v1 {test_v1 0}setNameV2(u)fmt.Println("v2", u) // v2 {test_v1 0}}
但在Golang中,這所謂的“引用傳遞”其實本質(zhì)上是值傳遞,因為這時候也發(fā)生了拷貝,只不過這時拷貝的是指針,而不是變量的值,所以“Golang的引用傳遞其實是引用的拷貝”。
?
可以通過以下代碼驗證:
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
type User struct {Name stringAge int}// 注意這里有個誤區(qū),我一開始看 user(v1)打印后的地址和一開始(init)是一致的,從而以為這是引用傳遞// 其實這里的user應(yīng)該看做一個指針變量,我們需要對比的是它的地址,所以還要再取一次地址func setNameV1(user *User) {fmt.Printf("v1: %p ", user) // 0xc0000a4018 與 init的地址一致fmt.Printf("v1_p: %p ", &user) // 0xc0000ac020user.Name = "test_v1"}// 值傳遞func setNameV2(user User) {fmt.Printf("v2_p: %p ", &user) //0xc0000a4030user.Name = "test_v2"}func main() {u := User{Name: "init"}up := &ufmt.Printf("init: %p ", up) //0xc0000a4018setNameV1(up)setNameV2(u)}
注:slice、map等本質(zhì)也是如此。
?
3.for range與指針
?
for range是在Golang中用于遍歷元素,當(dāng)它與指針結(jié)合時,稍不留神就會踩坑,這里有一段經(jīng)典代碼:
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
type User struct {Name stringAge int}func main() {userList := []User {User{Name: "aa", Age: 1},User{Name: "bb", Age: 1},}var newUser []*Userfor _, u := range userList {newUser = append(newUser, &u)}// 第一次:bb// 第二次:bbfor _, nu := range newUser {fmt.Printf("%+v", nu.Name)}}
按照正常的理解,應(yīng)該第一次輸出aa,第二次輸出bb,但實際上兩次都輸出了bb,這是因為 for range 的時候,變量u實際上只初始化了一次(每次遍歷的時候u都會被重新賦值,但是地址不變),導(dǎo)致每次append的時候,添加的都是同一個內(nèi)存地址,所以最終指向的都是最后一個值bb。
?
我們可以通過打印指針地址來驗證:
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
?類似的錯誤在Goroutine也經(jīng)常發(fā)生:func main() {userList := []User {User{Name: "aa", Age: 1},User{Name: "bb", Age: 1},}var newUser []*Userfor _, u := range userList {fmt.Printf("point: %p ", &u)fmt.Printf("val: %s ", u.Name)newUser = append(newUser, &u)}}// 最終輸出結(jié)果如下:point: 0xc00000c030val: aapoint: 0xc00000c030val: bb
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
// 這里要注意下,理論上這里都應(yīng)該輸出10的,但有可能出現(xiàn)執(zhí)行到7或者其他值的時候就輸出了,所以實際上這里不完全都輸出10func main() {for i := 0; i < 10; i++ {go func(idx *int) {fmt.Println("go: ", *idx)}(&i)}time.Sleep(5 * time.Second)}
4.閉包與指針
?
什么是閉包,一個函數(shù)和對其周圍狀態(tài)(lexical environment,詞法環(huán)境)的引用捆綁在一起(或者說函數(shù)被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個內(nèi)層函數(shù)中訪問到其外層函數(shù)的作用域。
?
當(dāng)閉包與指針進行結(jié)合時,如果閉包里面是一個指針變量,則外部變量的改變,也會影響到該閉包,起到意想不到的效果,讓我們繼續(xù)在舉幾個例子進行說明:
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
func incr1(x *int) func() {return func() {*x = *x + 1 // 這里是一個指針fmt.Printf("incr point x = %d ", *x)}}func incr2(x int) func() {return func() {x = x + 1fmt.Printf("incr normal x = %d ", x)}}func main() {x := 1i1 := incr1(&x)i2 := incr2(x)i1() // point x = 2i2() // normal x = 2i1() // point x = 3i2() // normal x = 3x = 100i1() // point x = 101 // 閉包1的指針變量受外部影響,被重置為100,并繼續(xù)遞增i2() // normal x = 4i1() // point x = 102i2() // normal x = 5}
5.指針與內(nèi)存逃逸
?
內(nèi)存逃逸的場景有很多,這里只討論由指針引發(fā)的內(nèi)存逃逸。理想情況下,肯定是盡量減少內(nèi)存逃逸,因為這意味著GC(垃圾回收)的壓力會減小,程序也會運行得更快。不過,使用指針又能減少內(nèi)存的占用,所以這本質(zhì)是內(nèi)存和GC的權(quán)衡,需要合理使用。
?
下面是指針引發(fā)的內(nèi)存逃逸的三種場景(歡迎大家補充~)
?
第一種場景:函數(shù)返回局部變量的指針
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
type Escape struct {Num1 intStr1 *stringSlice []int}// 返回局部變量的指針func NewEscape() *Escape {return &Escape{} // &Escape{} escapes to heap}func main() {e := &Escape{Num1: 0}}
第二種場景:被已經(jīng)逃逸的變量引用的指針
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
func main() {e := NewEscape()e.SetNum1(10)name := "aa"// e.Str1 中,e是已經(jīng)逃逸的變量, &name是被引用的指針e.Str1 = &name // moved to heap: name}
第三種場景:被指針類型的slice、map和chan引用的指針
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
func main() {e := NewEscape()e.SetNum1(10)name := "aa"e.Str1 = &name// 指針類型的slicearr := make([]*int, 2)n := 10 // moved to heap: narr[0] = &n // 被引用的指針}
歡迎大家繼續(xù)補充指針的其他注意事項~
審核編輯:湯梓紅
電子發(fā)燒友App









評論