## 12.5. 通過reflect.Value脩改值
到目前爲止,反射還隻是程序中變量的另一種訪問方式。然而,在本節中我們將重點討論如果通過反射機製來脩改變量。
迴想一下,Go語言中類似x、x.f[1]和*p形式的表達式都可以表示變量,但是其它如x + 1和f(2)則不是變量。一個變量就是一個可尋址的內存空間,里面存儲了一個值,併且存儲的值可以通過內存地址來更新。
對於reflect.Values也有類似的區别。有一些reflect.Values是可取地址的;其它一些則不可以。考慮以下的聲明語句:
```Go
x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)
```
其中a對應的變量則不可取地址。因爲a中的值僅僅是整數2的拷貝副本。b中的值也同樣不可取地址。c中的值還是不可取地址,它隻是一個指針`&x`的拷貝。實際上,所有通過reflect.ValueOf(x)返迴的reflect.Value都是不可取地址的。但是對於d,它是c的解引用方式生成的,指向另一個變量,因此是可取地址的。我們可以通過調用reflect.ValueOf(&x).Elem(),來獲取任意變量x對應的可取地址的Value。
我們可以通過調用reflect.Value的CanAddr方法來判斷其是否可以被取地址:
```Go
fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"
```
每當我們通過指針間接地獲取的reflect.Value都是可取地址的,卽使開始的是一個不可取地址的Value。在反射機製中,所有關於是否支持取地址的規則都是類似的。例如,slice的索引表達式e[i]將隱式地包含一個指針,它就是可取地址的,卽使開始的e表達式不支持也沒有關繫。以此類推,reflect.ValueOf(e).Index(i)對於的值也是可取地址的,卽使原始的reflect.ValueOf(e)不支持也沒有關繫。
要從變量對應的可取地址的reflect.Value來訪問變量需要三個步驟。第一步是調用Addr()方法,它返迴一個Value,里面保存了指向變量的指針。然後是在Value上調用Interface()方法,也就是返迴一個interface{},里面通用包含指向變量的指針。最後,如果我們知道變量的類型,我們可以使用類型的斷言機製將得到的interface{}類型的接口強製環爲普通的類型指針。這樣我們就可以通過這個普通指針來更新變量了:
```Go
x := 2
d := reflect.ValueOf(&x).Elem() // d refers to the variable x
px := d.Addr().Interface().(*int) // px := &x
*px = 3 // x = 3
fmt.Println(x) // "3"
```
或者,不使用指針,而是通過調用可取地址的reflect.Value的reflect.Value.Set方法來更新對於的值:
```Go
d.Set(reflect.ValueOf(4))
fmt.Println(x) // "4"
```
Set方法將在運行時執行和編譯時類似的可賦值性約束的檢査。以上代碼,變量和值都是int類型,但是如果變量是int64類型,那麽程序將拋出一個panic異常,所以關鍵問題是要確保改類型的變量可以接受對應的值:
```Go
d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
```
通用對一個不可取地址的reflect.Value調用Set方法也會導致panic異常:
```Go
x := 2
b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
```
這里有很多用於基本數據類型的Set方法:SetInt、SetUint、SetString和SetFloat等。
```Go
d := reflect.ValueOf(&x).Elem()
d.SetInt(3)
fmt.Println(x) // "3"
```
從某種程度上説,這些Set方法總是盡可能地完成任務。以SetInt爲例,隻要變量是某種類型的有符號整數就可以工作,卽使是一些命名的類型,隻要底層數據類型是有符號整數就可以,而且如果對於變量類型值太大的話會被自動截斷。但需要謹慎的是:對於一個引用interface{}類型的reflect.Value調用SetInt會導致panic異常,卽使那個interface{}變量對於整數類型也不行。
```Go
x := 1
rx := reflect.ValueOf(&x).Elem()
rx.SetInt(2) // OK, x = 2
rx.Set(reflect.ValueOf(3)) // OK, x = 3
rx.SetString("hello") // panic: string is not assignable to int
rx.Set(reflect.ValueOf("hello")) // panic: string is not assignable to int
var y interface{}
ry := reflect.ValueOf(&y).Elem()
ry.SetInt(2) // panic: SetInt called on interface Value
ry.Set(reflect.ValueOf(3)) // OK, y = int(3)
ry.SetString("hello") // panic: SetString called on interface Value
ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"
```
當我們用Display顯示os.Stdout結構時,我們發現反射可以越過Go語言的導出規則的限製讀取結構體中未導出的成員,比如在類Unix繫統上os.File結構體中的fd int成員。然而,利用反射機製併不能脩改這些未導出的成員:
```Go
stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var
fmt.Println(stdout.Type()) // "os.File"
fd := stdout.FieldByName("fd")
fmt.Println(fd.Int()) // "1"
fd.SetInt(2) // panic: unexported field
```
一個可取地址的reflect.Value會記録一個結構體成員是否是未導出成員,如果是的話則拒絶脩改操作。因此,CanAddr方法併不能正確反映一個變量是否是可以被脩改的。另一個相關的方法CanSet是用於檢査對應的reflect.Value是否是可取地址併可被脩改的:
```Go
fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"
```
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 査找重複的行
- GIF動畵
- 獲取URL
- 併發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 複數
- 布爾型
- 字符串
- 常量
- 複合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模闆
- 函數
- 函數聲明
- 遞歸
- 多返迴值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基於指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基於類型斷言識别錯誤類型
- 通過類型斷言査詢接口
- 類型分支
- 示例: 基於標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 併發的Clock服務
- 示例: 併發的Echo服務
- Channels
- 併發的循環
- 示例: 併發的Web爬蟲
- 基於select的多路複用
- 示例: 併發的字典遍歷
- 併發的退出
- 示例: 聊天服務
- 基於共享變量的併發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- sync.Once初始化
- 競爭條件檢測
- 示例: 併發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 爲何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value脩改值
- 示例: 解碼S表達式
- 獲取結構體字段標識
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附録