<Go>9 反射、网络编程

本文最后更新于:2024年7月7日 晚上

9 反射、网络编程

9.1 反射

反射可以在运行时动态获取变量信息,包括类型、类别。如果是结构体,还能获取其字段、方法。

通过反射,可以操作任意类型的对象。可以修改变量的值,调用关联的方法。

变量相关的内容包含在 reflect 包中

import "reflect"

reflect.Value

在 reflect.Value 结构体中,包含了与变量相关的信息

相关函数

  • ValueOf(i interface{}) Value:返回指定对象的 reflect.Value 结构体

    i := 100
    v := reflect.ValueOf(i)
  • (v Value) Elem() Value:返回该指针对象指向的对象的 reflect.Value 结构体

  • (v Value) Index(i int) Value:返回该切片的指定下标元素的 reflect.Value 结构体。必须是切片或数组

  • (v Value) Int() int64:获取该有符号数对象包含的值。

    对象本身不是 int、int 64… 等有符号数时,会产生 panic

    其余类型也有对应方法,如:Bool() bool

  • (v Value) SetInt(i int64):将对象包含的有符号数值改为指定的值。适用所有 int、int16… 等有符号数

    其余类型也有对应方法,如:SetBool(b bool)

    通用方法 (v Value) Set(i Value):将对象包含的值改为指定的值

    对象必须是一个指针类型的 Value,用 Elem() 方法获取的值:

    i := 1
    v := reflect.ValueOf(&i)
    v.Elem().SetInt(1000)
    fmt.Println(i)					// 1000
    
    type A struct{ i int }
    a := A{0}
    b := A{-100}
    v := reflect.ValueOf(&a)
    v.Elem().Set(reflect.ValueOf(&b).Elem())
    fmt.Println(a)					// {-100}
  • (v Value) Method(i int) Value:获得结构体的第 i 个方法

    方法是依据其方法名字符的 ASCII 码按照 字典顺序进行排序 的。

    只有首字母大写的方法才被计入

    (v Value) NumMethod() int:获得该结构体的方法数量

    (v Value) MethodByName(name string) Value:按标识符获得结构体的方法

  • (v Value) Field(i int) Value:返回结构体的第 i 个字段。也是字典顺序。

    (v Value) NumField() int:获得该结构体的字段数量

    (v Value) FieldByName(name string) Value:按标识符获得结构体的字段

  • (v Value) Call(in []Value) []Value:传入指定参数,并调用这个方法

Value 的转换:

变量、interface{}、reflect.Value 是可以相互转换的:

graph LR
A(变量) --赋值---> B(interface) --ValueOf 函数---> C(reflect.Value)
C --Interface 方法---> B --类型断言---> A

interface{} 通过 reflect.ValueOf() 函数转换为 reflect.Value

reflect.Value 通过 Interface() 方法转换为 interface{}

而 interface{} 通过类型断言可以转换为原本变量类型

i := 100						// i 的类型是 int
iv := reflect.ValueOf(i)		// iv 的类型是 reflect.Value
ii := iv.Interface()			// ii 的类型是 interface{}
i0 := ii.(int)					// i0 的类型是 int
fmt.Println(i, iv, ii, i0)		// 100 100 100 100

reflect.Type

在 reflect.Type 接口中,包含了与类型相关的信息

相关函数

  • TypeOf(i interface{}) Type:返回指定对象的 reflect.Type 接口

    i := 100
    t := reflect.TypeOf(i)
  • (v Value) Type() Type:返回该 Value 的对象的 Type 接口

  • (t Type) Name() string:返回指定对象的类型

  • (t Type) Field(i int) :获取该结构体的第 i 个字段

    获取这个字段的 tag:

    type A struct {
    	I int `json:"tag_of_A_I"`
    }
    
    func main() {
    	a := A{1}
    	t := reflect.TypeOf(a)
    	fmt.Println(t.Field(0).Tag.Get("json"))		// tag_of_A_I
    }
  • (t Type) NumIn() int:获取指定 func 的参数个数。对象必须是 func

    (t Type) NumOut() int:获取指定 func 的返回值个数

  • (t Type) In(i int) Type:获取指定 func 的第 i 个参数的类型。对象必须是 func

    (t Type) Out(i int) Type:获取指定 func 的第 i 个返回值的类型

  • New (t Type) Value:这是 reflect 包下的一个函数。

    创建一个指定类型的变量,其值为 该类型的零值指针

  • MakeSlice(t Type, len int, cap int):这是 reflect 包下的一个函数。

    创建一个指定类型的切片,其大小、容量是指定的值。

    同样还有 MakeChan(t Type, buffer int)MakeMap(t Type)MakeFunc(t Type, func(args []Value) (results []Value)) Value

    另外,值得一提的是,要想获取一个切片的元素的类型,可以用以下方式(不知道有没有更好方法):

    sli := reflect.TypeOf([]int{})     //  <---- sli 是一个任意的切片的 Type 接口
    ele := reflect.MakeSlice(sli, 1, 1).Index(0).Type()
    									// 创建了一个切片示例,之后取其第一个元素的类型

reflect.Kind

reflect.Kind 是一组常量,其指示了一些类别。

相关函数

  • (v Value) Kind() Kind:返回 Value 对象的分类。类别是 reflect.Kind

    (t Type) Kind() Kind:在 Type 中也实现了这个方法

Kind 是一组常量(一组数字),对 Kind 进行比较的方法:

i := 100
k := reflect.ValueOf(i).Kind()
if k == reflect.Int {
    fmt.Println("is int")
} else if k == reflect.Map {
    fmt.Println("is map")
}

Kind 与 Type 的区别:

下面的四组例子说明了 Kind 与 Type 的区别:

m := map[int]int{}
fmt.Println(reflect.TypeOf(m))				// map[int]int
fmt.Println(reflect.ValueOf(m).Kind())		// map

i := 100
fmt.Println(reflect.TypeOf(i))				// int
fmt.Println(reflect.ValueOf(i).Kind())		// int

p := &i
fmt.Println(reflect.TypeOf(i))				// *int
fmt.Println(reflect.ValueOf(i).Kind())		// ptr

type A struct{}
a := A{}
fmt.Println(reflect.TypeOf(a))         		// main.A
fmt.Println(reflect.ValueOf(a).Kind()) 		// struct

Kind 与 Type 可能相同,也可能不同。Type 即变量的类型,而 Kind 是变量的类别。

9.2 网络编程

网络编程有两种:

  • TCP Socket 编程

    是网络编程的主流。底层是 TCP/IP 协议。

  • b/s 结构的 HTTP 编程

    使用浏览器访问服务器时,就是使用 HTTP 协议。HTTP 协议底层仍是 TCP/IP 协议

net 包中,提供了与网络开发相关的方法

import "net"

net 包提供了可移植的网络 I/O 接口,包括 TCP/IP、UDP、域名解析和 Unix 域 Socket

相关接口 / 结构体

  • Listener:网络监听器

    type Listener interface {
        // Addr 返回该接口的网络地址
        Addr() Addr
        
        // Accept 等待并返回下一连接到该接口的连接
        Accept() (c Conn, err error)
        
        // Close 关闭该接口,使任何阻塞的 Accept 操作不再阻塞并返回错误
        Close() error
    }
  • Conn:面向流的网络连接

    type Conn interface {
        // Read 从连接中读取数据。可能在超时后返回错误,该错误的 Timeout() 返回 true
        Read(b []byte) (n int, err error)
        
        // Write 向连接中写入数据。可能在超时后返回错误,该错误的 Timeout() 返回 true
        Write(b []byte) (n int, err error)
        
        // Close 关闭该连接。此后任何阻塞的 Read 或 Write 不再阻塞或返回错误
        Close() error
        
        // LocalAddr 返回本地网络地址
        LocalAddr() Addr
        
        // RemoteAddr 返回远端网络地址
        RemoteAddr() Addr
        
        // SetDeadline 设定读写时限。超时后,I/O 操作会失败而不会阻塞。0 表示不设时限
        SetDeadline(t time.Time) error
        
        // SetReadDeadline 设定读操作的等待时限
        SetReadDeadline(t time.Time) error
        
        // SetWriteDeadline 设定写操作的等待时限
        SetWriteDeadline(t time.Time) error
    }
  • Addr:代表一个网络终端地址

    type Addr interface {
        // Network 返回其网络名
        Network() string
        
        // String 以字符串格式返回地址
        String() string
    }

9.2.1 服务端

持续监听一个指定端口。每当端口捕捉到通讯请求时,启动一个协程处理那个通信请求。

相关函数

  • func Listen(net, laddr string) (Listner, error)

    返回一个在本地网络地址 laddr 上监听的 Listener。未监听到时会持续阻塞

    net 是网络类型。其可以是:“tcp”、“tcp4”、“tcp6”、“unix”、“unixpacket”

示例

func server() {
	port := "0.0.0.0:9016"
	lis, err := net.Listen("tcp", port)					// [1]
	if err != nil {
		fmt.Println("异……常……")
		return
	} else {
		fmt.Println("向着星辰……")
	}
    
	defer lis.Close()									// [7]
	for {
		conn, e := lis.Accept()							// [2]
		if e != nil {
			fmt.Println("异……常……")
		} else {
			fmt.Println(conn.RemoteAddr().String(), "通讯请求")	// [3]
			go serve(conn)
		}
	}
}

func serve(conn net.Conn) {
	defer func() {
		recover()
		conn.Close()
	}()
	defer conn.Close()									// [6]
    
	data := make([]byte, 1024)
	for {
		n, err := conn.Read(data)						// [4]
		if err == nil {
			fmt.Println(conn.RemoteAddr().String(), "消息:", string(data[:n]))
		} else {
			if err == io.EOF {							// [5]
				fmt.Println(conn.RemoteAddr().String(), "已退出")
			} else {
				panic(err)
			}
			return
		}
	}
}
  1. func Listen(net, laddr string) (Listner, error)

    指定方式、端口,获得 Listener

  2. func (l *Listener) Accept() (c Conn, err error)

    用指定端口持续监听连接请求,获得 Conn。没有连接请求时会阻塞。

  3. func (c *Conn) RemoteAddr() Addr

    获取指定连接中的远端地址

    func (a *Addr) String() string

    获得表示该地址的字符串

  4. func (c *Conn) Read(b []byte) (n int, err error)

    持续地从连接中读取输入内容。没有输入内容时会阻塞。

  5. io.EOF

    当那个连接中断时,返回该错误信息

  6. func (c *Conn) Close() error

    func (l *Listener) Close() error

    通讯结束后,应关闭连接

9.2.2 客户端

向指定地址发送通信请求。

相关函数

  • func Dial(network, address string) (Conn, error)

    在网络 network 上连接地址 address,返回一个 Conn 接口

    network 可以是:“tcp”、“tcp4”、“tcp6”、“udp”、“udp4”、"udp6、“ip”、“ip4”、“ip6”、“unix”、“unixgram”、“unixpacket”

    对于 TCP 和 UDP 网络,地址格式为 host:port[host]:port。如:

    net.Dial("tcp", "0.0.0.0:9000")

    对于 IP 网络,器 network 必须是 “ip”、“ip4”、“ip6” 后跟冒号和协议号或协议名,地址需是 IP 地址字面值。如:

    net.Dial("ip6:ospf", "::1")

示例

import (
	"os"
    "bufio"
)

func client() {
	port := "0.0.0.0:9016"
	conn, err := net.Dial("tcp", port)				// [1]
	if err == nil {
		defer conn.Close()							// [5]
		for {
			read := bufio.NewReader(os.Stdin)		// [2]
			s, _ := read.ReadString('\n')			// [3]
			conn.Write([]byte(s))					// [4]
		}
	} else {
		fmt.Println("前面的区域,以后再探索吧!")
	}
}
  1. func Dial(network, address string) (Conn, error)

    尝试与指定地址进行通讯。己方的端口由系统随机分配

  2. func NewReader(rd io.Reader) *Reader

    获取一个读取键盘输入的输入流

    os.Stdin

    指向标准输入的文件。标准输入的文件默认是键盘输入

  3. func (b *Reader) ReadString(delim byte) (line string, err error)

    从上述键盘输入流获取输入的字符串

  4. func (c *Conn) Write(b []byte) (n int, err error)

    向连接中写入数据

  5. func (c *Conn) Close() error

    通讯结束后,应关闭连接

附录

力扣测试用例解析代码

该代码模拟力扣题目的 ”运行“ 模块。虽然不会给出正确答案,但是可以通过控制台追踪代码的运行情况。

给出节点定义的题目不能使用。

/* 这是使用方法的举例 */
func Max(a, b int) int {		// <---待测试函数
	if a > b {
		return a
	}
	return b
}

func sum(ns []int) (res int) {	// <---待测试函数
	for _, v := range ns {
		res += v
	}
	return
}

func main() {
	TestFuncS(sum, "[1,2,3,4]")
	TestFuncS(Max, "1,900")
}


//————————————————————————————————————————————————————————————————————————————


/* 这是测试方法的入口。先解析测试用例,那之后会执行函数。
   参数(functionName):测试代码的对象(函数名),要求是一个无隐参的函数
   参数(paraString):测试用例字符串。不同元素间由逗号分隔
   */
func TestFuncS(functionName interface{}, paraString string) {
	TestFunc(functionName, []byte(paraString))
}

func TestFunc(functionName interface{}, paraString []byte) {
	fun := reflect.ValueOf(functionName)

    // 检查是不是一个函数
	if fun.Kind() != reflect.Func {
		panic(errors.New("传入无效的函数名"))
	}
    
    // 获取该函数需要的参数
	paraNeeded := make([]reflect.Value, 0)
	for i := 0; i < fun.Type().NumIn(); i++ {
		paraNeeded = append(paraNeeded, reflect.New(fun.Type().In(i)))
	}
    
    // 解析测试用例字符串,得到需要的参数
	para := parsePara(paraString, paraNeeded...)
    
    // 调用函数。如有返回值,在屏幕上输出那个返回值
	for _, v := range fun.Call(para) {
		fmt.Print(v, "\t")
	}
	fmt.Println()
}


/* 这是一个解析方法。传入字符串会被拆分成几部分,每部分对应一个元素。
   参数(str):待解析的字符串。认为方括号内是一个完整切片。此外,不同元素间有逗号分隔
   参数(para):提示了解析后每个元素的正确类型。解析切片内部的元素时,仅以一个参数提示切片元素的类型
   */
func parsePara(paraString []byte, parameters ...reflect.Value) []reflect.Value {
	// 由 TestFunc 方法调用
	paraString = append(paraString, ',')	// 这步和代码逻辑有关,为了解析最后一个元素,在末尾添加一个额外的逗号
	res, length := make([]reflect.Value, 0), len(parameters)
	if length == 0 {
		panic(errors.New("参数长度为0"))	// 参数长度不太可能为0,以防万一加入了这个异常
	}
    
    // 每次解析字符串的第一项元素,之后将这部分字符串剔除。字符串变为空则解析完成
	for len(paraString) > 0 {
		if paraString[0] == '[' {
            // 解析一个切片类型
			val := parameters[len(res)%length]	// val 应该是一个切片指针的反射对象
			if val.Elem().Kind() != reflect.Slice {
				panic(errors.New("传入的参数有误"))	// 检查 para 参数是否提示是切片
			}
            
            // 完整获得一组方括号内的字符串,并解析
			count := 0
			for i := range paraString {
				if paraString[i] == '[' {
					count++
				} else if paraString[i] == ']' {
					count--
					if count == 0 {
						res = append(res, confirmPara([]byte(strings.TrimSpace(string(paraString[1:i]))), val))
						paraString = paraString[i+2:]	// 将解析完毕的部分剔除
						break
					}
				}
			}
		} else {
            // 解析一个基本数据类型
			for i := range paraString {
				if paraString[i] == ',' {
					res = append(res, confirmPara([]byte(strings.TrimSpace(string(paraString[:i]))), parameters[len(res)%length]))
					paraString = paraString[i+1:]	// 将解析完毕的部分剔除
					break
				}
			}
		}
	}
	return res
}


/* 这个方法确认一个元素的值
   参数(str):待解析的字符串。仅包含该项元素对应的部分字符串。
   参数(oriVal):仅提示该元素的正确类型
   */
// 由 TestFunc 方法(间接)调用
func confirmPara(stringPart []byte, elementType reflect.Value) reflect.Value {
    // val 是将来要返回的,最终会被用于调用函数的那个元素值的反射对象
	val := reflect.New(elementType.Elem().Type())
    
    // 下面开始根据元素类型采用不同的解析策略
	var err error = nil
	switch val.Elem().Kind() {
	case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
		var n int64
		n, err = strconv.ParseInt(string(stringPart), 10, 64)
		val.Elem().SetInt(n)
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		var n uint64
        // 考虑是否是 rune 与 byte 类型,做出不同处理
		if len(stringPart) == 3 && (stringPart[0] == '\'' || stringPart[0] == '"') {
			n = uint64(stringPart[1])
		} else {
			n, err = strconv.ParseUint(string(stringPart), 10, 64)
		}
		val.Elem().SetUint(n)
	case reflect.String:
		val.Elem().SetString(string(stringPart[1 : len(stringPart)-1]))	// 会去掉字符串的引号
	case reflect.Bool:
		var n bool
		n, err = strconv.ParseBool(string(stringPart))
		val.Elem().SetBool(n)
	case reflect.Float32, reflect.Float64:
		var n float64
		n, err = strconv.ParseFloat(string(stringPart), 64)
		val.Elem().SetFloat(n)
	default:
        // 不是上述类型时,认为传入类型是某种切片。调用方法继续拆解该切片字符串
		n := parsePara(stringPart, reflect.New(reflect.MakeSlice(val.Elem().Type(), 1, 1).Index(0).Type()))
		temp := reflect.MakeSlice(val.Elem().Type(), len(n), len(n))
		for i := range n {
			temp.Index(i).Set(n[i])
		}
		val.Elem().Set(temp)
	}
	if err != nil {
		panic(err)
	}
	return val.Elem()
}

<Go>9 反射、网络编程
https://i-melody.github.io/2022/07/25/Go/9 反射、网络编程/
作者
Melody
发布于
2022年7月25日
许可协议