最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

深入了解Golang的指針用法

 更新時(shí)間:2022年07月08日 15:06:41   作者:言淦  
與C語言一樣,Go語言中同樣有指針,通過指針,我們可以只傳遞變量的內(nèi)存地址,而不是傳遞整個(gè)變量,這在一定程度上可以節(jié)省內(nèi)存的占用。本文將通過示例詳細(xì)講講Golang的指針用法,需要的可以參考一下

與C語言一樣,Go語言中同樣有指針,通過指針,我們可以只傳遞變量的內(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)文章

  • go語言實(shí)現(xiàn)同步操作項(xiàng)目示例

    go語言實(shí)現(xiàn)同步操作項(xiàng)目示例

    本文主要介紹了go語言實(shí)現(xiàn)同步操作項(xiàng)目示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-05-05
  • 詳解go如何優(yōu)雅的使用接口與繼承

    詳解go如何優(yōu)雅的使用接口與繼承

    Go語言中的接口和嵌套結(jié)構(gòu)體是兩種重要的代碼設(shè)計(jì)方式,接口定義了一組方法簽名,使得不同的類型能夠以相同的方式進(jìn)行交互,本文將給大家介紹go語言如何優(yōu)雅的使用接口與繼承,文中有詳細(xì)的代碼供大家參考,需要的朋友可以參考下
    2024-06-06
  • Go語言中DateTime的用法介紹

    Go語言中DateTime的用法介紹

    這篇文章介紹了Go語言中DateTime的用法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-07-07
  • golang 實(shí)現(xiàn)對(duì)Map進(jìn)行鍵值自定義排序

    golang 實(shí)現(xiàn)對(duì)Map進(jìn)行鍵值自定義排序

    這篇文章主要介紹了golang 實(shí)現(xiàn)對(duì)Map進(jìn)行鍵值自定義排序,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Golang 字符串轉(zhuǎn)time類型實(shí)現(xiàn)

    Golang 字符串轉(zhuǎn)time類型實(shí)現(xiàn)

    本文主要介紹了Golang 字符串轉(zhuǎn)time類型實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • Go1.18?新特性之多模塊Multi-Module工作區(qū)模式

    Go1.18?新特性之多模塊Multi-Module工作區(qū)模式

    這篇文章主要介紹了Go1.18?新特性之多模塊Multi-Module工作區(qū)模式,在 Go 1.18之前,建議使用依賴模塊中的 replace 指令來處理這個(gè)問題,從 Go 1.18開始引入了一種同時(shí)處理多個(gè)模塊的新方法,通過案例給大家詳細(xì)介紹,感興趣的朋友一起看看吧
    2022-04-04
  • 詳解Go語言中for循環(huán),break和continue的使用

    詳解Go語言中for循環(huán),break和continue的使用

    這篇文章主要通過一些示例為大家介紹一下Go語言中for循環(huán)、break和continue的基本語法以及使用,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下
    2022-06-06
  • Golang實(shí)現(xiàn)優(yōu)雅的將struct轉(zhuǎn)換為map

    Golang實(shí)現(xiàn)優(yōu)雅的將struct轉(zhuǎn)換為map

    在項(xiàng)目實(shí)踐中,有時(shí)候我們需要將struct結(jié)構(gòu)體轉(zhuǎn)為map映射表,然后基于map做數(shù)據(jù)裁剪或操作。那么下面我來介紹下常用的兩種轉(zhuǎn)換方式,希望對(duì)大家有所幫助
    2023-01-01
  • Go Asynq異步任務(wù)處理的實(shí)現(xiàn)

    Go Asynq異步任務(wù)處理的實(shí)現(xiàn)

    Asynq是一個(gè)新興的異步任務(wù)處理解決方案,它提供了輕量級(jí)的、易于使用的API,本文主要介紹了Go Asynq異步任務(wù)處理的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-06-06
  • Go語言中make和new函數(shù)的用法與區(qū)別

    Go語言中make和new函數(shù)的用法與區(qū)別

    這篇文章介紹了Go語言中make和new函數(shù)的用法與區(qū)別,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-07-07

最新評(píng)論

九龙坡区| 花莲县| 额敏县| 石渠县| 司法| 石棉县| 玛曲县| 明水县| 神农架林区| 黄梅县| 通山县| 台安县| 柯坪县| 泾川县| 邛崃市| 鹰潭市| 克山县| 平和县| 龙口市| 勃利县| 常山县| 深泽县| 湖南省| 古交市| 甘洛县| 达尔| 晋城| 泰和县| 于田县| 沛县| 平武县| 香格里拉县| 夹江县| 襄汾县| 金华市| 静乐县| 汝州市| 桂林市| 镇巴县| 穆棱市| 铜陵市|