Golang:使用 reflect 包讀寫各類型變量

Go 在標準庫中提供的 reflect 包 [2] 讓 Go 程序具備運行時的反射能力 (reflection)[3],但這種反射能力也是一把 “雙刃劍”,它在解決一類特定問題方面具有優勢,但也帶來了邏輯不清晰、性能問題以及難於發現問題和調試等不足。不過從 Go 誕生伊始就隨着 Go 一起發佈的 reflect 包是 Go 不可或缺的重要能力,不管你是否使用,都要掌握使用 reflect 與類型系統交互的基本方法,比如在反射的世界裏如何讀寫各類型變量。本文就來和大家快速過一遍使用 reflect 包讀寫 Go 基本類型變量、複合類型變量的方法以及它們的應用。

1. 基本類型

進入 reflect 世界的大門主要有兩個:reflect.ValueOf 和 reflect.TypeOf。進入到反射世界,每個變量都能找到一個與自己的對應的 reflect.Value,通過該 Value 我們可以讀寫真實世界的變量信息。這裏主要和大家過一遍操作各類型變量值的方法,因此主要用到的是 reflect.ValueOf。

Go 原生基本類型 (非複合類型) 主要包括:

我們在反射的世界裏如何獲取這些類型變量的值,又或如何在反射的世界裏修改這些變量的值呢?下面這個示例可以作爲日常使用 reflect 讀寫 Go 基本類型變量的速查表:

// github.com/bigwhite/experiments/blob/master/vars-in-reflect/basic/main.go

package main

import (
 "fmt"
 "reflect"
)

func main() {
 // 整型
 var i int = 11
 vi := reflect.ValueOf(i)                         // reflect Value of i
 fmt.Printf("i = [%d], vi = [%d]\n", i, vi.Int()) // i = [11]vi = [11]
 // vi.SetInt(11 + 100) // panic: reflect: reflect.Value.SetInt using unaddressable value

 vai := reflect.ValueOf(&i) // reflect Value of Address of i
 vi = vai.Elem()
 fmt.Printf("i = [%d], vi = [%d]\n", i, vi.Int()) // i = [11]vi = [11]
 vi.SetInt(11 + 100)
 fmt.Printf("after set, i = [%d]\n", i) // after set, i = [111]

 // 整型指針
 i = 11
 var pi *int = &i
 vpi := reflect.ValueOf(pi) // reflect Value of pi
 vi = vpi.Elem()
 vi.SetInt(11 + 100)
 fmt.Printf("after set, i = [%d]\n", i) // after set, i = [111]

 // 浮點型
 var f float64 = 3.1415

 vaf := reflect.ValueOf(&f)
 vf := vaf.Elem()
 fmt.Printf("f = [%f], vf = [%f]\n", f, vf.Float()) // f = [3.141500]vf = [3.141500]
 vf.SetFloat(100 + 3.1415)
 fmt.Printf("after set, f = [%f]\n", f) // after set, f = [103.141500]

 // 複數型
 var c = complex(5.1, 6.2)

 vac := reflect.ValueOf(&c)
 vc := vac.Elem()
 fmt.Printf("c = [%g], vc = [%g]\n", f, vc.Complex()) // c = [103.1415]vc = [(5.1+6.2i)]
 vc.SetComplex(complex(105.1, 106.2))
 fmt.Printf("after set, c = [%g]\n", c) // after set, c = [(105.1+106.2i)]

 // 布爾類型
 var b bool = true

 vab := reflect.ValueOf(&b)
 vb := vab.Elem()
 fmt.Printf("b = [%t], vb = [%t]\n", b, vb.Bool()) // b = [true]vb = [true]
 vb.SetBool(false)
 fmt.Printf("after set, b = [%t]\n", b) // after set, b = [false]

 // 字符串類型
 var s string = "hello, reflect"

 vas := reflect.ValueOf(&s)
 vs := vas.Elem()
 fmt.Printf("s = [%s], vs = [%s]\n", s, vs.String()) // s = [hello, reflect]vs = [hello, reflect]
 vs.SetString("bye, reflect")
 fmt.Printf("after set, s = [%s]\n", s) // after set, s = [bye, reflect]
}

我們看到:

2. 複合類型

前面我們已經看到,使用 reflect 包在反射世界讀寫原生基本類型的變量還是相對容易的多的,接下來我們再來看看複合類型 (Composite type) 變量的讀寫。

Go 中的複合類型包括:

與基本類型變量不同,複合變量多由同構和異構的字段 (field) 或元素 (element) 組成,如何讀寫複合類型變量中的字段或元素的值纔是我們需要考慮的問題。下面這個示例可作爲日常使用 reflect 在反射世界裏讀寫 Go 複合類型變量中字段或元素值的速查表:

// github.com/bigwhite/experiments/blob/master/vars-in-reflect/composite/main.go

package main

import (
 "fmt"
 "reflect"
 "unsafe"
)

type Foo struct {
 Name string
 age  int
}

func main() {
 // 數組
 var a = [5]int{1, 2, 3, 4, 5}
 vaa := reflect.ValueOf(&a) // reflect Value of Address of arr
 va := vaa.Elem()
 va0 := va.Index(0)
 fmt.Printf("a0 = [%d], va0 = [%d]\n", a[0], va0.Int()) // a0 = [1]va0 = [1]
 va0.SetInt(100 + 1)
 fmt.Printf("after set, a0 = [%d]\n", a[0]) // after set, a0 = [101]

 // 切片
 var s = []int{11, 12, 13}
 vs := reflect.ValueOf(s)
 vs0 := vs.Index(0)
 fmt.Printf("s0 = [%d], vs0 = [%d]\n", s[0], vs0.Int()) // s0 = [11]vs0 = [11]
 vs0.SetInt(100 + 11)
 fmt.Printf("after set, s0 = [%d]\n", s[0]) // after set, s0 = [111]

 // map
 var m = map[int]string{
  1: "tom",
  2: "jerry",
  3: "lucy",
 }

 vm := reflect.ValueOf(m)
 vm_1_v := vm.MapIndex(reflect.ValueOf(1))                      // the reflect Value of the value of key 1
 fmt.Printf("m_1 = [%s], vm_1 = [%s]\n", m[1], vm_1_v.String()) // m_1 = [tom]vm_1 = [tom]
 vm.SetMapIndex(reflect.ValueOf(1), reflect.ValueOf("tony"))
 fmt.Printf("after set, m_1 = [%s]\n", m[1]) // after set, m_1 = [tony]

 // 爲map m新增一組key-value
 vm.SetMapIndex(reflect.ValueOf(4), reflect.ValueOf("amy"))
 fmt.Printf("after set, m = [%#v]\n", m) // after set, m = [map[int]string{1:"tony", 2:"jerry", 3:"lucy", 4:"amy"}]

 // 結構體
 var f = Foo{
  Name: "lily",
  age:  16,
 }

 vaf := reflect.ValueOf(&f)
 vf := vaf.Elem()
 field1 := vf.FieldByName("Name")
 fmt.Printf("the Name of f = [%s]\n", field1.String()) // the Name of f = [lily]
 field2 := vf.FieldByName("age")
 fmt.Printf("the age of f = [%d]\n", field2.Int()) // the age of f = [16]

 field1.SetString("ally")
 // field2.SetInt(8) // panic: reflect: reflect.Value.SetInt using value obtained using unexported field
 nAge := reflect.NewAt(field2.Type(), unsafe.Pointer(field2.UnsafeAddr())).Elem()
 nAge.SetInt(8)
 fmt.Printf("after set, f is [%#v]\n", f) // after set, f is [main.Foo{Name:"ally", age:8}]

 // 接口
 var g = Foo{
  Name: "Jordan",
  age:  40,
 }

 // 接口底層動態類型爲複合類型變量
 var i interface{} = &g
 vi := reflect.ValueOf(i)
 vg := vi.Elem()

 field1 = vg.FieldByName("Name")
 fmt.Printf("the Name of g = [%s]\n", field1.String()) // the Name of g = [Jordan]
 field2 = vg.FieldByName("age")
 fmt.Printf("the age of g = [%d]\n", field2.Int()) // the age of g = [40]

 nAge = reflect.NewAt(field2.Type(), unsafe.Pointer(field2.UnsafeAddr())).Elem()
 nAge.SetInt(50)
 fmt.Printf("after set, g is [%#v]\n", g) // after set, g is [main.Foo{Name:"Jordan", age:50}]

 // 接口底層動態類型爲基本類型變量
 var n = 5
 i = &n
 vi = reflect.ValueOf(i).Elem()
 fmt.Printf("i = [%d], vi = [%d]\n", n, vi.Int()) // i = [5]vi = [5]
 vi.SetInt(10)
 fmt.Printf("after set, n is [%d]\n", n) // after set, n is [10]

 // channel
 var ch = make(chan int, 100)
 vch := reflect.ValueOf(ch)
 vch.Send(reflect.ValueOf(22))

 j := <-ch
 fmt.Printf("recv [%d] from channel\n", j) // recv [22] from channel

 ch <- 33
 vj, ok := vch.Recv()
 fmt.Printf("recv [%d] ok[%t]\n", vj.Int(), ok) // recv [33] ok[true]
}

從上述示例,我們可以得到如下一些信息:

        nAge = reflect.NewAt(field2.Type(), unsafe.Pointer(field2.UnsafeAddr())).Elem()
        nAge.SetInt(50)

我們通過 reflect.NewAt 創建了一個新 Value 實例,該實例表示指向 field2 地址的指針。然後通過 Elem 方法,我們得到該指針 Value 指向的對象的 Value:nAge,實際就是 field2 變量。然後通過 nAge 設置的新值也將反映在 field2 的值上。這和上面基本類型那個示例中的 vpi 和 vi 的功用類似。

3. 獲取系統資源描述符的值

reflect 包的一大功用就是獲取一些被封裝在底層的系統資源描述符的值,比如:socket 描述符、文件描述符。

a) 文件描述符

os.File 提供了 Fd 方法用於獲取文件對應的 os 底層的文件描述符的值。我們也可以使用反射來實現同樣的功能:

// github.com/bigwhite/experiments/blob/master/vars-in-reflect/system-resource/file_fd.go
package main

import (
 "fmt"
 "os"
 "reflect"
)

func fileFD(f *os.File) int {
 file := reflect.ValueOf(f).Elem().FieldByName("file").Elem()
 pfdVal := file.FieldByName("pfd")
 return int(pfdVal.FieldByName("Sysfd").Int())
}

func main() {
 fileName := os.Args[1]
 f, err := os.Open(fileName)
 if err != nil {
  panic(err)
 }

 defer f.Close()

 fmt.Printf("file descriptor is %d\n", f.Fd())
 fmt.Printf("file descriptor in reflect is %d\n", fileFD(f))
}

執行上述示例:

$go build file_fd.go 
$./file_fd file_fd.go
file descriptor is 3
file descriptor in reflect is 3

我們看到通過 reflect 獲取到的 fd 值與通過 Fd 方法得到的值是一致的。

下面我們可以基於上面對讀寫基本類型和複合類型變量的理解來簡單分析一下 fileFD 函數的實現:

os.File 的定義如下:

// $GOROOT/src/os/types.go

type File struct {
        *file // os specific
}

爲了通過反射獲取到未導出指針變量 file,我們使用下面反射語句:

 file := reflect.ValueOf(f).Elem().FieldByName("file").Elem()

有了上面的 Value 實例 file,我們就可以繼續反射 os.file 結構了。os.file 結構是因 os 而異的,以 linux/mac 的 unix 爲例,os.file 的結構如下:

// $GOROOT/src/os/file_unix.go

type file struct {
        pfd         poll.FD
        name        string
        dirinfo     *dirInfo // nil unless directory being read
        nonblock    bool     // whether we set nonblocking mode
        stdoutOrErr bool     // whether this is stdout or stderr
        appendMode  bool     // whether file is opened for appending
}

於是我們繼續反射:

 pfdVal := file.FieldByName("pfd")

而 poll.FD 的結構如下:

// $GOROOT/src/internal/poll/fd_unix.go

// field of a larger type representing a network connection or OS file.
type FD struct {
        // Lock sysfd and serialize access to Read and Write methods.
        fdmu fdMutex

        // System file descriptor. Immutable until Close.
        Sysfd int

        // I/O poller.
        pd pollDesc

        // Writev cache.
        iovecs *[]syscall.Iovec 
    
        // Semaphore signaled when file is closed.
        csema uint32

        // Non-zero if this file has been set to blocking mode.
        isBlocking uint32

        // Whether this is a streaming descriptor, as opposed to a
        // packet-based descriptor like a UDP socket. Immutable.
        IsStream bool

        // Whether a zero byte read indicates EOF. This is false for a
        // message based socket connection.
        ZeroReadIsEOF bool

        // Whether this is a file rather than a network socket.
        isFile bool
}

這其中的 Sysfd 記錄的就是系統的文件描述符的值,於是通過下面語句即可得到該文件描述符的值:

 return int(pfdVal.FieldByName("Sysfd").Int())

b) socket 描述符

unix 下一切皆文件!socket 描述符也是一個文件描述符,並且 Go 並沒有在標準庫中直接提供獲取 socket 文件描述符的 API。我們只能通過反射獲取。看下面示例:

// github.com/bigwhite/experiments/blob/master/vars-in-reflect/system-resource/socket_fd.go

package main

import (
 "fmt"
 "log"
 "net"
 "reflect"
)

func socketFD(conn net.Conn) int {
 tcpConn := reflect.ValueOf(conn).Elem().FieldByName("conn")
 fdVal := tcpConn.FieldByName("fd")
 pfdVal := fdVal.Elem().FieldByName("pfd")
 return int(pfdVal.FieldByName("Sysfd").Int())
}

func main() {

 ln, err := net.Listen("tcp"":8080")
 if err != nil {
  panic(err)
 }

 for {
  conn, err := ln.Accept()
  if err != nil {
   if ne, ok := err.(net.Error); ok && ne.Temporary() {
    log.Printf("accept temp err: %v", ne)
    continue
   }

   log.Printf("accept err: %v", err)
   return
  }

  fmt.Printf("conn fd is [%d]\n", socketFD(conn))
 }
}

我們看到 socketFD 的實現與 fileFD 的實現有些類似,我們從 net.Conn 一步步反射得到底層的 Sysfd。

傳給 socketFD 的實參實質是一個 TCPConn 實例,通過 reflect.ValueOf(conn).Elem() 我們可以獲取到該實例在反射世界的 Value

// $GOROOT/src/net/tcpsock.go

type TCPConn struct {
        conn
}

然後再通過 FieldByName("conn") 得到 TCPConn 結構中字段 conn 在反射世界中的 Value。net.conn 結構如下:

// $GOROOT/src/net/net.go
type conn struct {
        fd *netFD
}

起鬨的 netFD 是一個 os 相關的結構,以 linux/mac 爲例,其結構如下:

// $GOROOT/src/net/fd_posix.go

// Network file descriptor.
type netFD struct {
        pfd poll.FD

        // immutable until Close
        family      int
        sotype      int
        isConnected bool // handshake completed or use of association with peer
        net         string
        laddr       Addr
        raddr       Addr
}

我們又看到了 poll.FD 類型字段 pfd,再往下的反射就和 fileFD 一致了。

本文涉及的源碼可以在這裏 [5] 下載:https://github.com/bigwhite/experiments/blob/master/vars-in-reflect

參考資料

[1] 

本文永久鏈接: https://tonybai.com/2021/04/19/variable-operation-using-reflection-in-go

[2] 

reflect 包: https://www.imooc.com/read/87/article/2474

[3] 

運行時的反射能力 (reflection): https://www.imooc.com/read/87/article/2474

[4] 

其底層實現爲指針類型結構: https://www.imooc.com/read/87/article/2383

[5] 

這裏: https://github.com/bigwhite/experiments/blob/master/vars-in-reflect

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/EnKX6xQdx4kqvAC0G--kNA