<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
}
}
}
-
func Listen(net, laddr string) (Listner, error)
指定方式、端口,获得 Listener
-
func (l *Listener) Accept() (c Conn, err error)
用指定端口持续监听连接请求,获得 Conn。没有连接请求时会阻塞。
-
func (c *Conn) RemoteAddr() Addr
获取指定连接中的远端地址
func (a *Addr) String() string
获得表示该地址的字符串
-
func (c *Conn) Read(b []byte) (n int, err error)
持续地从连接中读取输入内容。没有输入内容时会阻塞。
-
io.EOF
当那个连接中断时,返回该错误信息
-
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("前面的区域,以后再探索吧!")
}
}
-
func Dial(network, address string) (Conn, error)
尝试与指定地址进行通讯。己方的端口由系统随机分配
-
func NewReader(rd io.Reader) *Reader
获取一个读取键盘输入的输入流
os.Stdin
指向标准输入的文件。标准输入的文件默认是键盘输入
-
func (b *Reader) ReadString(delim byte) (line string, err error)
从上述键盘输入流获取输入的字符串
-
func (c *Conn) Write(b []byte) (n int, err error)
向连接中写入数据
-
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()
}