go reflect要不要傳指針原理詳解
正文
在我們看一些使用反射的代碼的時(shí)候,會(huì)發(fā)現(xiàn),reflect.ValueOf
或 reflect.TypeOf
的參數(shù)有些地方使用的是指針參數(shù),有些地方又不是指針參數(shù), 但是好像這兩者在使用上沒(méi)什么區(qū)別,比如下面這樣:
var a = 1 v1 := reflect.ValueOf(a) v2 := reflect.ValueOf(&a) fmt.Println(v1.Int()) // 1 fmt.Println(v2.Elem().Int()) // 1
它們的區(qū)別貌似只是需不需要使用 Elem()
方法,但這個(gè)跟我們是否傳遞指針給 reflect.ValueOf
其實(shí)關(guān)系不大, 相信沒(méi)有人為了使用一下 Elem()
方法,就去傳遞指針給 reflect.ValueOf
吧。
那我們什么時(shí)候應(yīng)該傳遞指針參數(shù)呢?
什么時(shí)候傳遞指針?
要回答這個(gè)問(wèn)題,我們可以思考一下以下列出的幾點(diǎn)內(nèi)容:
- 是否要修改變量的值,要修改就要用指針
- 結(jié)構(gòu)體類(lèi)型:是否要修改結(jié)構(gòu)體里的字段,要修改就要用指針
- 結(jié)構(gòu)體類(lèi)型:是否要調(diào)用指針接收值方法,要調(diào)用就要用指針
- 對(duì)于
chan
、map
、slice
類(lèi)型,我們傳遞值和傳遞指針都可以修改其內(nèi)容 - 對(duì)于非
interface
類(lèi)型,傳遞給TypeOf
和ValueOf
的時(shí)候都會(huì)轉(zhuǎn)換為interface
類(lèi)型,如果本身就是interface
類(lèi)型,則不需轉(zhuǎn)換。 - 指針類(lèi)型不可修改,但是可以修改指針指向的值。(
v := reflect.ValueOf(&a)
,v.CanSet()
是false
,v.Elem().CanSet()
是true
) - 字符串:我們可以對(duì)字符串進(jìn)行替換,但不能修改字符串的某一個(gè)字符
大概總結(jié)下來(lái),就是:如果我們想修改變量的內(nèi)容,就傳遞指針,否則就傳遞值。對(duì)于某些復(fù)合類(lèi)型如果其內(nèi)部包含了底層數(shù)據(jù)的指針, 也是可以通過(guò)傳值來(lái)修改其底層數(shù)據(jù)的,這些類(lèi)型有 chan
、map
、slice
。 又或者如果我們想修改結(jié)構(gòu)體類(lèi)型里面的指針類(lèi)型字段,傳遞結(jié)構(gòu)體的拷貝也能實(shí)現(xiàn)。
1. 通過(guò)傳遞指針修改變量的值
對(duì)于一些基礎(chǔ)類(lèi)型的變量,如果我們想修改其內(nèi)容,就要傳遞指針。這是因?yàn)樵?go 里面參數(shù)傳遞都是值傳遞,如果我們不傳指針, 那么在函數(shù)內(nèi)部拿到的只是參數(shù)的拷貝,對(duì)其進(jìn)行修改,不會(huì)影響到外部的變量(事實(shí)上在對(duì)這種反射值進(jìn)行修改的時(shí)候會(huì)直接 panic
)。
傳值無(wú)法修改變量本身
x := 1 v := reflect.ValueOf(x)
在這個(gè)例子中,v
中保存的是 x
的拷貝,對(duì)這份拷貝在反射的層面上做修改其實(shí)是沒(méi)有實(shí)際意義的,因?yàn)閷?duì)拷貝進(jìn)行修改并不會(huì)影響到 x
本身。 我們?cè)谕ㄟ^(guò)反射來(lái)修改變量的時(shí)候,我們的預(yù)期行為往往是修改變量本身。鑒于實(shí)際的使用場(chǎng)景,go 的反射系統(tǒng)已經(jīng)幫我們做了限制了, 在我們對(duì)拷貝類(lèi)型的反射對(duì)象進(jìn)行修改的時(shí)候,會(huì)直接 panic
。
傳指針可以修改變量
x := 1 v := reflect.ValueOf(&x).Elem()
在這個(gè)例子中,我們傳遞了 x
的指針到 reflect.ValueOf
中,這樣一來(lái),v
指向的就是 x
本身了。 在這種情況下,我們對(duì) v
的修改就會(huì)影響到 x
本身。
2. 通過(guò)傳遞指針修改結(jié)構(gòu)體的字段
對(duì)于結(jié)構(gòu)體類(lèi)型,如果我們想修改其字段的值,也是要傳遞指針的。這是因?yàn)榻Y(jié)構(gòu)體類(lèi)型的字段是值類(lèi)型,如果我們不傳遞指針, reflect.ValueOf
拿到的也是一份拷貝,對(duì)其進(jìn)行修改并不會(huì)影響到結(jié)構(gòu)體本身。當(dāng)然,這種情況下,我們修改它的時(shí)候也會(huì) panic
。
type person struct { Name string Age int } p := person{ Name: "foo", Age: 30, } // v 本質(zhì)上是指向 p 的指針 v := reflect.ValueOf(&p) // v.CanSet() 為 false,v 是指針,指針本身是不能修改的 // v.Elem() 是 p 本身,是可以修改的 fmt.Println(v.Elem().FieldByName("Name").CanSet()) // true fmt.Println(v.Elem().FieldByName("Age").CanSet()) // true
3. 結(jié)構(gòu)體:獲取指針接收值方法
對(duì)于結(jié)構(gòu)體而言,如果我們想通過(guò)反射來(lái)調(diào)用指針接收者方法,那么我們需要傳遞指針。
在開(kāi)始講解這一點(diǎn)之前,需要就以下內(nèi)容達(dá)成共識(shí):
type person struct { } func (p person) M1() { } func (p *person) M2() { } func TestPerson(t *testing.T) { p := person{} v1 := reflect.ValueOf(p) v2 := reflect.ValueOf(&p) assert.Equal(t, 1, v1.NumMethod()) assert.Equal(t, 2, v2.NumMethod()) // v1 和 v2 都有 M1 方法 assert.True(t, v1.MethodByName("M1").IsValid()) assert.True(t, v2.MethodByName("M1").IsValid()) // v1 沒(méi)有 M2 方法 // v2 有 M2 方法 assert.False(t, v1.MethodByName("M2").IsValid()) assert.True(t, v2.MethodByName("M2").IsValid()) }
在上面的代碼中,p
只有一個(gè)方法 M1
,而 &p
有兩個(gè)方法 M1
和 M2
。 但是在實(shí)際使用中,我們使用 p 來(lái)調(diào)用 M2 也是可以的, p
之所以能調(diào)用 M2
是因?yàn)榫幾g器幫我們做了一些處理,將 p
轉(zhuǎn)換成了 &p
,然后調(diào)用 M2
。
但是在反射的時(shí)候,我們是無(wú)法做到這一點(diǎn)的,這個(gè)需要特別注意。如果我們想通過(guò)反射來(lái)調(diào)用指針接收者的方法,就需要傳遞指針。
4. 變量本身包含指向數(shù)據(jù)的指針
最好不要通過(guò)值的反射對(duì)象來(lái)修改值的數(shù)據(jù),就算有些類(lèi)型可以實(shí)現(xiàn)這種功能。
對(duì)于 chan
、map
、slice
這三種類(lèi)型,我們可以通過(guò) reflect.ValueOf
來(lái)獲取它們的值, 但是這個(gè)值本身包含了指向數(shù)據(jù)的指針,因此我們依然可以通過(guò)反射系統(tǒng)修改其數(shù)據(jù)。但是,我們最好不這么用,從規(guī)范的角度,這是一種錯(cuò)誤的操作。
通過(guò)值反射對(duì)象修改 chan、map 和 slice
在 go 中,chan
、map
、slice
這幾種數(shù)據(jù)結(jié)構(gòu)中,存儲(chǔ)數(shù)據(jù)都是通過(guò)一個(gè) unsafe.Pointer
類(lèi)型的變量來(lái)指向?qū)嶋H存儲(chǔ)數(shù)據(jù)的內(nèi)存。 這是因?yàn)?,這幾種類(lèi)型能夠存儲(chǔ)的元素個(gè)數(shù)都是不確定的,都需要根據(jù)我們指定的大小和存儲(chǔ)的元素類(lèi)型來(lái)進(jìn)行內(nèi)存分配。
正因如此,我們復(fù)制 chan
、map
、slice
的時(shí)候,雖然值被復(fù)制了一遍,但是存儲(chǔ)數(shù)據(jù)的指針也被復(fù)制了, 這樣我們依然可以通過(guò)拷貝的數(shù)據(jù)指針來(lái)修改其數(shù)據(jù),如下面的例子:
func TestPointer1(t *testing.T) { // 數(shù)組需要傳遞引用才能修改其元素 arr := [3]int{1, 2, 3} v1 := reflect.ValueOf(&arr) v1.Elem().Index(1).SetInt(100) assert.Equal(t, 100, arr[1]) // chan 傳值也可以修改其元素 ch := make(chan int, 1) v2 := reflect.ValueOf(ch) v2.Send(reflect.ValueOf(10)) assert.Equal(t, 10, <-ch) // map 傳值也可以修改其元素 m := make(map[int]int) v3 := reflect.ValueOf(m) v3.SetMapIndex(reflect.ValueOf(1), reflect.ValueOf(10)) assert.Equal(t, 10, m[1]) // slice 傳值也可以修改其元素 s := []int{1, 2, 3} v4 := reflect.ValueOf(s) v4.Index(1).SetInt(20) assert.Equal(t, 20, s[1]) }
slice 反射對(duì)象擴(kuò)容的影響
但是,我們需要注意的是,對(duì)于 map
和 slice
類(lèi)型,在其分配的內(nèi)存容納不下新的元素的時(shí)候,會(huì)進(jìn)行擴(kuò)容, 擴(kuò)容之后,保存數(shù)據(jù)字段的指針就指向了一片新的內(nèi)存了。 這意味著什么呢?這意味著,我們通過(guò) map
和 slice
的值創(chuàng)建的反射值對(duì)象中拿到的那份數(shù)據(jù)指針已經(jīng)跟舊的 map
和 slice
指向的內(nèi)存不一樣了。
說(shuō)明:在上圖中,我們?cè)诜瓷鋵?duì)象中往 slice
追加元素后,導(dǎo)致反射對(duì)象 slice
的 array
指針指向了一片新的內(nèi)存區(qū)域了, 這個(gè)時(shí)候我們?cè)賹?duì)反射對(duì)象進(jìn)行修改的時(shí)候,不會(huì)影響到原 slice
。這也就是我們不能通過(guò) slice
或 map
的拷貝的反射對(duì)象來(lái)修改 slice
或 map
的原因。
示例代碼:
func TestPointer1(t *testing.T) { s := []int{1, 2, 3} v4 := reflect.ValueOf(s) v4.Index(1).SetInt(20) assert.Equal(t, 20, s[1]) // 這里發(fā)生了擴(kuò)容 // v5 的 array 跟 s 的 array 指向的是不同的內(nèi)存區(qū)域了。 v5 := reflect.Append(v4, reflect.ValueOf(4)) fmt.Println(s) // [1 20 3] fmt.Println(v5.Interface().([]int)) // [1 20 3 4] // 這里修改 v5 的時(shí)候影響不到 s 了 v5.Index(1).SetInt(30) fmt.Println(s) // [1 20 3] fmt.Println(v5.Interface().([]int)) // [1 30 3 4] }
說(shuō)明:在上面的代碼中,v5
實(shí)際上是 v4
擴(kuò)容后的切片,底層的 array
指針指向的是跟 s
不一樣的 array
了, 因此在我們修改 v5
的時(shí)候,會(huì)發(fā)現(xiàn)原來(lái)的 s
并沒(méi)有發(fā)生改變。
雖然通過(guò)值反射對(duì)象可以修改 slice 的數(shù)據(jù),但是如果通過(guò)反射對(duì)象 append 元素到 slice 的反射對(duì)象的時(shí)候, 可能會(huì)觸發(fā) slice 擴(kuò)容,這個(gè)時(shí)候再修改反射對(duì)象的時(shí)候,就影響不了原來(lái)的 slice 了。
slice 容量夠的話(huà)是不是就可以正常追加元素了?
只能說(shuō),能,也不能。我們看看下面這個(gè)例子:
func TestPointer000(t *testing.T) { s1 := make([]int, 3, 6) s1[0] = 1 s1[1] = 2 s1[2] = 3 fmt.Println(s1) // [1 2 3] v6 := reflect.ValueOf(s1) v7 := reflect.Append(v6, reflect.ValueOf(4)) // 雖然 s1 的容量足夠大,但是 s1 還是看不到追加的元素 fmt.Println(s1) // [1 2 3] fmt.Println(v7.Interface().([]int)) // [1 2 3 4] // s1 和 s2 底層數(shù)組還是同一個(gè) // array1 是 s1 底層數(shù)組的內(nèi)存地址 array1 := (*(*reflect.SliceHeader)(unsafe.Pointer(&s1))).Data s2 := v7.Interface().([]int) // array2 是 s2 底層數(shù)組的內(nèi)存地址 array2 := (*(*reflect.SliceHeader)(unsafe.Pointer(&s2))).Data assert.Equal(t, array1, array2) // 這是因?yàn)?s1 的長(zhǎng)度并沒(méi)有發(fā)生改變, // 所以 s1 看不到追加的那個(gè)元素 fmt.Println(len(s1), cap(s1)) // 3 6 fmt.Println(len(s2), cap(s2)) // 4 6 }
在這個(gè)例子中,我們給 slice
分配了足夠大的容量,但是我們通過(guò)反射對(duì)象來(lái)追加元素的時(shí)候, 雖然數(shù)據(jù)被正常追加到了 s1
底層數(shù)組,但是由于在反射對(duì)象以外的 s1
的 len
并沒(méi)有發(fā)生改變, 因此 s1
還是看不到反射對(duì)象追加的元素。所以上面說(shuō)可以正常追加元素。
但是,外部由于 len
沒(méi)有發(fā)生改變,因此外部看不到反射對(duì)象追加的元素,所以上面也說(shuō)不能正常追加元素。
因此,雖然理論上修改的是同一片內(nèi)存,我們依然不能通過(guò)傳值的方式來(lái)通過(guò)反射對(duì)象往 slice
中追加元素。 但是修改 [0, len(s))
范圍內(nèi)的元素在反射對(duì)象外部是可以看到的。
map 也不能通過(guò)值反射對(duì)象來(lái)修改其元素。
跟 slice
類(lèi)似,通過(guò) map
的值反射對(duì)象來(lái)追加元素的時(shí)候,同樣可能導(dǎo)致擴(kuò)容, 擴(kuò)容之后,保存數(shù)據(jù)的內(nèi)存區(qū)域會(huì)發(fā)生改變。
但是,從另一個(gè)角度看,如果我們只是修改其元素的話(huà),是可以正常修改的。
chan 沒(méi)有追加
chan
跟 slice
、map
有個(gè)不一樣的地方,它的長(zhǎng)度是我們創(chuàng)建 chan
的時(shí)候就已經(jīng)固定的了, 因此,不存在擴(kuò)容導(dǎo)致指向內(nèi)存區(qū)域發(fā)生改變的問(wèn)題。
因此,對(duì)于 chan
類(lèi)型的元素,我們傳 ch
或者 &ch
給 reflect.ValueOf
都可以實(shí)現(xiàn)修改 ch
。
結(jié)構(gòu)體字段包含指針的情況
如果結(jié)構(gòu)體里面包含了指針字段,我們也只是想通過(guò)反射對(duì)象來(lái)修改這個(gè)指針字段的話(huà), 那么我們也還是可以通過(guò)傳值給 reflect.ValueOf
來(lái)創(chuàng)建反射對(duì)象來(lái)修改這個(gè)指針字段:
type person struct { Name *string } func TestPointerPerson(t *testing.T) { name := "foo" p := person{Name: &name} v := reflect.ValueOf(p) fmt.Println(v.Field(0).Elem().CanAddr()) fmt.Println(v.Field(0).Elem().CanSet()) name1 := "bar" v.Field(0).Elem().Set(reflect.ValueOf(name1)) // p 的 Name 字段已經(jīng)被成功修改 fmt.Println(*p.Name) }
在這個(gè)例子中,我們雖然使用了 p
而不是 &p
來(lái)創(chuàng)建反射對(duì)象, 但是我們依然可以修改 Name
字段,因?yàn)榉瓷鋵?duì)象拿到了 Name
的指針的拷貝, 通過(guò)這個(gè)拷貝是可以定位到 p
的 Name
字段本身指向的內(nèi)存的。
但是我們依然是不能修改 p
中的其他字段。
5. interface 類(lèi)型處理
對(duì)于 interface
類(lèi)型的元素,我們可以將以下兩種操作看作是等價(jià)的:
// v1 跟 v2 都拿到了 a 的拷貝 var a = 1 v1 := reflect.ValueOf(a) var b interface{} = a v2 := reflect.ValueOf(b)
我們可以通過(guò)下面的斷言來(lái)證明:
assert.Equal(t, v1.Kind(), v2.Kind()) assert.Equal(t, v1.CanAddr(), v2.CanAddr()) assert.Equal(t, v1.CanSet(), v2.CanSet()) assert.Equal(t, v1.Interface(), v2.Interface())
當(dāng)然,對(duì)于指針類(lèi)型也是一樣的:
// v1 跟 v2 都拿到了 a 的指針 var a = 1 v1 := reflect.ValueOf(&a) var b interface{} = &a v2 := reflect.ValueOf(b)
同樣的,我們可以通過(guò)下面的斷言來(lái)證明:
assert.Equal(t, v1.Kind(), v2.Kind()) assert.Equal(t, v1.Elem().Kind(), v2.Elem().Kind()) assert.Equal(t, v1.Elem().CanAddr(), v2.Elem().CanAddr()) assert.Equal(t, v1.Elem().Addr(), v2.Elem().Addr()) assert.Equal(t, v1.Interface(), v2.Interface()) assert.Equal(t, v1.Elem().Interface(), v2.Elem().Interface())
interface 底層類(lèi)型是值
interface
類(lèi)型的底層類(lèi)型是值的時(shí)候,我們將其傳給 reflect.ValueOf
跟直接傳值是一樣的。 是沒(méi)有辦法修改 interface
底層數(shù)據(jù)的值的(除了指針類(lèi)型字段,因?yàn)榉瓷鋵?duì)象也拿到了指針字段的地址):
type person struct { Name *string } func TestInterface1(t *testing.T) { name := "foo" p := person{Name: &name} // v 拿到的是 p 的拷貝 // 下面兩行等價(jià)于 v := reflect.ValueOf(p) var i interface{} = p v := reflect.ValueOf(i) assert.False(t, v.CanAddr()) assert.Equal(t, reflect.Struct, v.Kind()) assert.True(t, v.Field(0).Elem().CanAddr()) }
在上面這個(gè)例子中 v := reflect.ValueOf(i)
其實(shí)等價(jià)于 v := reflect.ValueOf(p)
, 因?yàn)樵谖覀冋{(diào)用 reflect.ValueOf(p)
的時(shí)候,go 語(yǔ)言本身會(huì)幫我們將 p
轉(zhuǎn)換為 interface{}
類(lèi)型。 在我們賦值給 i
的時(shí)候,go 語(yǔ)言也會(huì)幫我們將 p
轉(zhuǎn)換為 interface{}
類(lèi)型。 這樣再調(diào)用 reflect.ValueOf
的時(shí)候就不需要再做轉(zhuǎn)換了。
interface 底層類(lèi)型是指針
傳遞底層數(shù)據(jù)是指針類(lèi)型的 interface
給 reflect.ValueOf
的時(shí)候,我們可以修改 interface
底層指針指向的值, 效果等同于直接傳遞指針給 reflect.ValueOf
:
func TestInterface(t *testing.T) { var a = 1 v1 := reflect.ValueOf(&a) var b interface{} = &a v2 := reflect.ValueOf(b) // v1 和 v2 本質(zhì)上都接收了一個(gè) interface 參數(shù), // 這個(gè) interface 參數(shù)的數(shù)據(jù)部分都是 &a v1.Elem().SetInt(10) assert.Equal(t, 10, a) // 通過(guò) v1 修改 a 的值,v2 也能看到 assert.Equal(t, 10, v2.Elem().Interface()) // 同樣的,通過(guò) v2 修改 a 的值,v1 也能看到 v2.Elem().SetInt(20) assert.Equal(t, 20, a) assert.Equal(t, 20, v1.Elem().Interface()) }
不要再對(duì)接口類(lèi)型取地址
能不能通過(guò)反射 Value 對(duì)象來(lái)修改變量只取決于,能不能根據(jù)反射對(duì)象拿到最初變量的內(nèi)存地址。 如果拿到的只是原始值的拷貝,不管我們?cè)趺醋龆紵o(wú)法修改原始值。
對(duì)于初學(xué)者另外一個(gè)令人困惑的地方可能是下面這樣的代碼:
func TestInterface(t *testing.T) { var a = 1 var i interface{} = a v1 := reflect.ValueOf(&a) v2 := reflect.ValueOf(&i) // v1 和 v2 的類(lèi)型都是 reflect.Ptr assert.Equal(t, reflect.Ptr, v1.Kind()) assert.Equal(t, reflect.Ptr, v2.Kind()) // 但是兩者的 Elem() 類(lèi)型不同, // v1 的 Elem() 是 reflect.Int, // v2 的 Elem() 是 reflect.Interface assert.Equal(t, reflect.Int, v1.Elem().Kind()) assert.Equal(t, reflect.Interface, v2.Elem().Kind()) }
困惑的源頭在于,reflect.ValueOf()
這個(gè)函數(shù)的參數(shù)是 interface{}
類(lèi)型的, 這意味著我們可以傳遞任意類(lèi)型的值給它,包括指針類(lèi)型的值。
正因如此,如果我們不懂得 reflect
包的工作原理的話(huà), 就會(huì)傳錯(cuò)變量到 reflect.ValueOf()
函數(shù)中,導(dǎo)致程序出錯(cuò)。
對(duì)于上面例子的 v2
,它是一個(gè)指向 interface{}
類(lèi)型的指針的反射對(duì)象,它也能找到最初的變量 a
:
但是能不能修改 a
,還是取決于 a
是否是可尋址的。也就是最初傳遞給 i
的值是不是一個(gè)指針類(lèi)型。
assert.Equal(t, "<*interface {} Value>", v2.String()) assert.Equal(t, "<interface {} Value>", v2.Elem().String()) assert.Equal(t, "<int Value>", v2.Elem().Elem().String())
在上面的例子中,我們傳遞給 i
的是 a
的值,而不是 a
的指針,所以 i
是不可尋址的,也就是說(shuō) v2
是不可尋址的。
上圖說(shuō)明:
i
是接口類(lèi)型,它的數(shù)據(jù)部分是a
的拷貝,它的類(lèi)型部分是int
類(lèi)型。&i
是指向接口的指針,它指向了上圖的eface
。v2
是指向eface
的指針的反射對(duì)象。- 最終,我們通過(guò)
v2
找到i
這個(gè)接口,然后通過(guò)i
找到a
這個(gè)變量的拷貝。
所以,繞了一大圈,我們最終還是修改不了 a
的值。到最后我們只拿到了 a
的拷貝。
6. 指針類(lèi)型反射對(duì)象不可修改其指向地址
其實(shí)這一點(diǎn)上面有些地方也有涉及到,但是這里再?gòu)?qiáng)調(diào)一下。一個(gè)例子如下:
func TestPointer(t *testing.T) { var a = 1 var b = &a v := reflect.ValueOf(b) assert.False(t, v.CanAddr()) assert.False(t, v.CanSet()) assert.True(t, v.Elem().CanAddr()) assert.True(t, v.Elem().CanSet()) }
說(shuō)明:
v
是指向&a
的指針的反射對(duì)象。- 通過(guò)這個(gè)反射對(duì)象的
Elem()
方法,我們可以找到原始的變量a
。 - 反射對(duì)象本身不能修改,但是它的
Elem()
方法返回的反射對(duì)象可以修改。
對(duì)于指針類(lèi)型的反射對(duì)象,其本身不能修改,但是它的 Elem()
方法返回的反射對(duì)象可以修改。
7. 反射也不能修改字符串中的字符
這是因?yàn)?,go 中的字符串本身是不可變的,我們無(wú)法像在 C 語(yǔ)言中那樣修改其中某一個(gè)字符。 其實(shí)不止是 go,其實(shí)很多編程語(yǔ)言的字符串都是不可變的,比如 Java 中的 String
類(lèi)型。
在 go 中,字符串是用一個(gè)結(jié)構(gòu)體來(lái)表示的,大概長(zhǎng)下面這個(gè)樣子:
type StringHeader struct { Data uintptr Len int }
Data
是指向字符串的指針。Len
是字符串的長(zhǎng)度(單位為字節(jié))。
在 go 中 str[1] = 'a'
這樣的操作是不允許的,因?yàn)樽址遣豢勺兊摹?/p>
相同的字符串只有一個(gè)實(shí)例
假設(shè)我們定義了兩個(gè)相同的字符串,如下:
s1 := "hello" s2 := "hello"
這兩個(gè)字符串的值是相同的,但是它們的地址是不同的。那既然如此,為什么我們還是不能修改它的其中某一個(gè)字符呢? 這是因?yàn)?,雖然 s1
和 s2
的地址不一樣,但是它們實(shí)際保存 hello
這個(gè)字符串的地址是一樣的:
v1 := (*reflect.StringHeader)(unsafe.Pointer(&s1)) v2 := (*reflect.StringHeader)(unsafe.Pointer(&s2)) // 兩個(gè)字符串實(shí)例保存字符串的內(nèi)存地址是一樣的 assert.Equal(t, v1.Data, v2.Data)
兩個(gè)字符串內(nèi)存表示如下:
所以,我們可以看到,s1
和 s2
實(shí)際上是指向同一個(gè)字符串的指針,所以我們無(wú)法修改其中某一個(gè)字符。 因?yàn)槿绻试S這種行為存在的話(huà),我們對(duì)其中一個(gè)字符串實(shí)例修改,也會(huì)影響到另外一個(gè)字符串實(shí)例。
字符串本身可以替換
雖然我們不能修改字符串中的某一個(gè)字符,但是我們可以通過(guò)反射對(duì)象把整個(gè)字符串替換掉:
func TestStirng(t *testing.T) { s := "hello" v := reflect.ValueOf(&s) fmt.Println(v.Elem().CanAddr()) fmt.Println(v.Elem().CanSet()) v.Elem().SetString("world") fmt.Println(s) // world }
這里實(shí)際上是把 s
中保存字符串的地址替換成了指向 world
這個(gè)字符串的地址,而不是將 hello
指向的內(nèi)存修改成 world
:
func TestStirng(t *testing.T) { s := "hello" oldAddr := (*reflect.StringHeader)(unsafe.Pointer(&s)).Data v := reflect.ValueOf(&s) v.Elem().SetString("world") newAddr := (*reflect.StringHeader)(unsafe.Pointer(&s)).Data // 修改之后,實(shí)際保存字符串的內(nèi)存地址發(fā)生了改變 assert.NotEqual(t, oldAddr, newAddr) }
這可以用下圖表示:
總結(jié)
- 如果我們需要通過(guò)反射對(duì)象來(lái)修改變量的值,那么我們必須得有辦法拿到變量實(shí)際存儲(chǔ)的內(nèi)存地址。這種情況下,很多時(shí)候都是通過(guò)傳遞指針給
reflect.ValueOf()
方法來(lái)實(shí)現(xiàn)的。 - 但是對(duì)于
chan
、map
和slice
或者其他類(lèi)似的數(shù)據(jù)結(jié)構(gòu),它們通過(guò)指針來(lái)引用實(shí)際存儲(chǔ)數(shù)據(jù)的內(nèi)存,這種數(shù)據(jù)結(jié)構(gòu)是通過(guò)通過(guò)傳值給reflect.ValueOf()
方法來(lái)實(shí)現(xiàn)修改其中的元素的。因?yàn)檫@些數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)部分可以通過(guò)指針的拷貝來(lái)修改。 - 但是
map
和slice
有可能會(huì)擴(kuò)容,如果通過(guò)反射對(duì)象來(lái)追加元素,可能導(dǎo)致追加失敗。這是因?yàn)椋ㄟ^(guò)反射對(duì)象追加元素的時(shí)候,如果擴(kuò)容了,那么原來(lái)的內(nèi)存地址就會(huì)失效,這樣我們其實(shí)就修改不了原來(lái)的map
和slice
了。 - 同樣的,結(jié)構(gòu)體傳值來(lái)創(chuàng)建反射對(duì)象的時(shí)候,如果其中有指針類(lèi)型的字段,那么我們也可以通過(guò)指針來(lái)修改其中的元素。但是其他字段也還是修改不了的。
- 如果我們創(chuàng)建反射對(duì)象的參數(shù)是
interface
類(lèi)型,那么能不能修改元素的變量還是取決于我們這個(gè)interface
類(lèi)型變量的數(shù)據(jù)部分是值還是指針。如果interface
變量中存儲(chǔ)的是值,那么我們就不能修改其中的元素了。如果interface
變量中存儲(chǔ)的是指針,就可以修改。 - 我們無(wú)法修改字符串的某一個(gè)字符,通過(guò)反射也不能,因?yàn)樽址旧硎遣豢勺兊?。不同?
stirng
類(lèi)型的變量,如果它們的值是一樣的,那么它們會(huì)共享實(shí)際存儲(chǔ)字符串的內(nèi)存。 - 但是我們可以直接用一個(gè)新的字符串替代舊的字符串。
但其實(shí)說(shuō)了那么多,簡(jiǎn)單來(lái)說(shuō)只有一點(diǎn),就是我們只能通過(guò)反射對(duì)象來(lái)修改指針類(lèi)型的變量。如果拿不到實(shí)際存儲(chǔ)數(shù)據(jù)的指針,那么我們就無(wú)法通過(guò)反射對(duì)象來(lái)修改其中的元素了。
以上就是go reflect要不要傳指針原理詳解的詳細(xì)內(nèi)容,更多關(guān)于go reflect 傳指針的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Go語(yǔ)言如何實(shí)現(xiàn)字符串切片反轉(zhuǎn)函數(shù)
Go?語(yǔ)言不像其他語(yǔ)言如?Python,有著內(nèi)置的?reverse()?函數(shù),本文將先學(xué)習(xí)一下Python中對(duì)于列表的反轉(zhuǎn)方法,然后再學(xué)習(xí)如果在Go語(yǔ)言中實(shí)現(xiàn)相同的功能,感興趣的小伙伴快跟隨小編一起來(lái)學(xué)習(xí)一下2022-10-10Go語(yǔ)言測(cè)試庫(kù)testify使用學(xué)習(xí)
這篇文章主要為大家介紹了Go語(yǔ)言測(cè)試庫(kù)testify的使用學(xué)習(xí)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07關(guān)于go平滑重啟庫(kù)overseer實(shí)現(xiàn)原理詳解
這篇文章主要為大家詳細(xì)介紹了關(guān)于go平滑重啟庫(kù)overseer實(shí)現(xiàn)原理,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,有需要的小伙伴可以參考下2023-11-11golang?chan傳遞數(shù)據(jù)的性能開(kāi)銷(xiāo)詳解
這篇文章主要為大家詳細(xì)介紹了Golang中chan在接收和發(fā)送數(shù)據(jù)時(shí)因?yàn)椤皬?fù)制”而產(chǎn)生的開(kāi)銷(xiāo),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2024-01-01Go 實(shí)現(xiàn)一次性打包各個(gè)平臺(tái)的可執(zhí)行程序
這篇文章主要介紹了Go 實(shí)現(xiàn)一次性打包各個(gè)平臺(tái)的可執(zhí)行程序,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12go?singleflight緩存雪崩源碼分析與應(yīng)用
這篇文章主要為大家介紹了go?singleflight緩存雪崩源碼分析與應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09