• Go学习第十二章——Go反射与TCP编程


    1 反射

    1.1 基本介绍

    **反射:**在编译时静态类型语言中实现动态特性的一种机制。

    Go语言通过反射提供了一种在运行时检查类型和访问类型成员的能力,它可以在不知道具体类型的情况下,动态地获取和修改变量的值、调用方法等。

    1. 反射可以咋子运行时动态获取变量的各种信息,比如:变量的类型,类别
    2. 如果是结构体遍历,还可以获取到结构体本身的信息(包括结构体的字段、方法)
    3. 通过反射,可以修改变量的值,可以调用关联的方法。
    4. 使用反射,需要 import (“reflect”)

    image-20231027142557365

    使用场景:

    1. JSON序列化和反序列化:根据结构体的字段和标签信息,通过反射可以动态地进行JSON的序列化和反序列化。
    2. ORM框架:通过反射可以解析结构体字段和数据库表的映射关系,实现自动化的ORM操作。
    3. 动态调用函数和方法:通过反射可以根据函数或方法的名称动态地进行调用。
    4. 接口的实现判断:通过反射可以判断一个类型是否实现了某个接口,从而在运行时动态地处理接口类型。
    5. 插件系统:通过反射可以在运行时动态加载和执行插件功能。
    1.2 快速入门

    反射中重要的函数

    1. reflect.TypeOf(变量名),获取变量的类型,返回reflect.Type类型
    2. reflect.ValueOf(变量名),获取变量的值,返回reflect.Value类型。reflect.Value 是一个结构体类型。

    注意:变量、interface{} 和 reflect.Value是可以相互转换的,这点在实际开发中,会经常使用到。

    image-20231027151101752

    1. 变量 转 interface{}
    var name string = "John"
    var i interface{} = name
    fmt.Printf("%T %v\n", i, i) // 输出: string John
    
    • 1
    • 2
    • 3
    1. interface{} 转 变量
    var j interface{} = "Tom"
    var name2 string
    name2, ok := j.(string)
    if ok {
    	fmt.Printf("%T %v\n", name2, name2) // 输出: string Tom
    } else {
    	fmt.Println("conversion failed")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. reflect.Value 转变量

    函数:func ValueOf(i interface{}) Value

    作用:ValueOf返回一个初始化为 i 接口保管的具体值的Value,ValueOf(nil)返回Value零值。

    var age int = 42
    value := reflect.ValueOf(age)
    valueInt := value.Interface()
    age2, ok := valueInt.(int)
    if ok {
    	fmt.Printf("%T %v\n", age2, age2) // 输出: int 42
    } else {
    	fmt.Println("conversion failed")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    详细讲解一下:

    • 首先定义了一个 age 变量,类型为 int,并赋值为 42。接下来使用 reflect.ValueOf() 函数将 age 转换为 reflect.Value 类型,并将结果赋值给变量 value

    • reflect.Value 是反射库 reflect 提供的一个类型,用于存储和操作变量的信息。它包含了变量的值以及其相关的类型信息

    • 为了能够方便地对变量进行操作,我们可以通过 reflect.Value 调用其 Interface() 方法,将其转换为 interface{} 类型的变量。这样就可以得到原本变量的值,只不过类型变为了 interface{}。将结果赋值给变量 valueInt

    1. 变量 转 reflect.Value
    var score float32 = 98.5
    value2 := reflect.ValueOf(score)
    fmt.Printf("%T %v\n", value2, value2) // 输出: reflect.Value
    
    • 1
    • 2
    • 3
    1. reflect.Value 转 interface{}
    var flag bool = true
    value3 := reflect.ValueOf(flag)
    valueInterface := value3.Interface()
    flag2, ok := valueInterface.(bool)
    if ok {
    	fmt.Printf("%T %v\n", flag2, flag2) // 输出: bool true
    } else {
    	fmt.Println("conversion failed")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 结构体 转 Interface{}, reflect.Value
    type Student struct {
        Name  string
        Age   int
        score float64
    }
    
    func main() {
        stu := Student{"Tom", 22, 88.4}
    
        // 结构体 转 interface{}
        var i interface{} = stu
        fmt.Printf("i的类型:%T ,值:%v\n", i, i)
    
        // 结构体 转 reflect.Value
        value := reflect.ValueOf(stu)
        fmt.Printf("value的类型:%T ,值:%v\n", value, value) 
        
        // reflect.Value 转 结构体
        valueInterface := value.Interface()
        // 断言,看看是否转换成功
        flag1, ok := valueInterface.(Student)
        if ok {
           fmt.Printf("flag1的类型:%T ,值:%v\n", flag1, flag1) 
        } else {
           fmt.Println("conversion failed")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    输出结果:

    i的类型:main.Student ,值:{Tom 22 88.4}
    value的类型:reflect.Value ,值:{Tom 22 88.4}
    flag1的类型:main.Student ,值:{Tom 22 88.4}
    
    • 1
    • 2
    • 3
    1.3 注意事项和细节说明

    补充:

    常量介绍

    1. 常量使用const 修饰
    2. 常量在定义的时候,必须初始化
    3. 常量不能修改
    4. 常量只能修饰bool、数值类型(int, float系列)、string类型
    5. 语法:const identifier [type] = value
    6. 举例说明:
    • const name = “tom” // ok
    • const tax float64 = 0.8 // ok
    • const a int // error
    • const b = 9 / 3 // ok
    • const c = getVal() // err
    1. 注意事项:

      1. Golang 中 没有常量名必须字母大写的规范,比如:TAX_RATE等

      2. 常量仍然通过首字母的大小写来控制常量的访问范围。

    反射的注意事项:

    1. reflect.Value.Kind,获取变量的类别,返回的是一个常量。

      方法:func (k Kind) String() string

      func main() {
      	stu := Student{"Tom", 18, 88.1}
      	stuType := reflect.TypeOf(stu)
      	kind := stuType.Kind()
      	fmt.Println("kind=", kind) // 输出:kind= struct
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    2. Type是类型,Kind是类别,Type 和 Kind 可能是相同的,也可能是不同的。

    比如:var num int = 10,num的Type是int,Kind也是int

    比如:var stu Student stu的Type是 包名.Student,Kind是struct

    1. 通过反射可以让变量在interface[]和Reflect.Value之间相互转换

    2. 使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如 x 是 int,那么就应该使用reflect.Value(x).Int(),而不能使用其他的,否则报panic。

    3. 通过反射来修改变量,注意当使用SetXxx方法来设置需要通过对应的指针类型来完成,这样才能改变传入的变量的值,同时需要使用到reflect.Value.Elem()方法。

    方法:func (v Value) Elem() Value

    作用:Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。如果v的Kind不是Interface或Ptr会panic;如果v持有的值为nil,会返回Value零值。

    func reflect01(i interface{}) {
    	// 2. 获取到reflect.Value
    	rVal := reflect.ValueOf(i)
    	// 看看 rVal的Kind是啥
    	fmt.Printf("rVal kind=%v\n", rVal.Kind())
    	// 3.通过这里直接设置,下面num的值
    	// rVal.SetInt(20) // 报错,这里的rVal是个指针
    	rVal.Elem().SetInt(20) // 成功
    }
    func main() {
    	var num int = 10
    	reflect01(&num)
    	fmt.Println("num=", num)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1.4 最佳实践

    案例:使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值。

    //定义了一个Monster结构体
    type Monster struct {
        Name  string `json:"name"`
        Age   int `json:"monster_age"`
        Score float32 `json:"成绩"`
        Sex   string
        
    }
    
    //方法,返回两个数的和
    func (s Monster) GetSum(n1, n2 int) int {
        return n1 + n2
    }
    //方法, 接收四个值,给s赋值
    func (s Monster) Set(name string, age int, score float32, sex string) {
        s.Name = name
        s.Age = age
        s.Score = score
        s.Sex = sex
    }
    
    //方法,显示s的值
    func (s Monster) Print() {
        fmt.Println("---start~----")
        fmt.Println(s)
        fmt.Println("---end~----")
    }
    func TestStruct(a interface{}) {
        //获取reflect.Type 类型
        typ := reflect.TypeOf(a)
        //获取reflect.Value 类型
        val := reflect.ValueOf(a)
        //获取到a对应的类别
        kd := val.Kind()
        //如果传入的不是struct,就退出
        if kd !=  reflect.Struct {
           fmt.Println("expect struct")
           return
        }
    
        //获取到该结构体有几个字段
        num := val.NumField()
    
        fmt.Printf("struct has %d fields\n", num) //4
        //变量结构体的所有字段
        for i := 0; i < num; i++ {
           fmt.Printf("Field %d: 值为=%v\n", i, val.Field(i))
           //获取到struct标签, 注意需要通过reflect.Type来获取tag标签的值
           tagVal := typ.Field(i).Tag.Get("json")
           //如果该字段于tag标签就显示,否则就不显示
           if tagVal != "" {
              fmt.Printf("Field %d: tag为=%v\n", i, tagVal)
           }
        }
        
        //获取到该结构体有多少个方法
        numOfMethod := val.NumMethod()
        fmt.Printf("struct has %d methods\n", numOfMethod)
        
        //var params []reflect.Value
        //方法的排序默认是按照 函数名的排序(ASCII码)
        val.Method(1).Call(nil) //获取到第二个方法。调用它
    
        
        //调用结构体的第1个方法Method(0)
        var params []reflect.Value  //声明了 []reflect.Value
        params = append(params, reflect.ValueOf(10))
        params = append(params, reflect.ValueOf(40))
        res := val.Method(0).Call(params) //传入的参数是 []reflect.Value, 返回[]reflect.Value
        fmt.Println("res=", res[0].Int()) //返回结果, 返回的结果是 []reflect.Value*/
    
    }
    func main() {
        //创建了一个Monster实例
        var a Monster = Monster{
           Name:  "黄鼠狼精",
           Age:   400,
           Score: 30.8,
        }
        //将Monster实例传递给TestStruct函数
        TestStruct(a)  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    输出结果:

    struct has 4 fields
    Field 0: 值为=黄鼠狼精
    Field 0: tag为=name
    Field 1: 值为=400
    Field 1: tag为=monster_age
    Field 2: 值为=30.8
    Field 2: tag为=成绩
    Field 3: 值为=
    struct has 3 methods
    ---start~----
    {黄鼠狼精 400 30.8 }
    ---end~----
    res= 50
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    过程并不复杂,主要的是TetsSturct函数里面的内容,就是徐徐渐进,直到获得对应的值。

    2 Tcp Socket编程

    这里需要前置知识,计算机网络部分的知识,如果没有,建议去好好看一下,再往下学~~

    2.1 基本介绍

    image-20231027165335796

    服务端处理流程:

    1. 监听端口 8888
    2. 接收客户端的tcp链接,简历客户端和服务端的链接
    3. 创建goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)

    客户端处理流程:

    1. 建立与服务端的链接
    2. 发送请求数据[终端],接收服务器端返回的结果数据
    3. 关闭链接

    简单的程序示意图:

    image-20231027165924840

    2.2 入门案例

    服务器端功能:

    1. 编写一个服务器程序,在8888端口监听
    2. 可以和多个客户端创建链接
    3. 链接成功后,客户端可以发送数据,服务器端接受数据,并显示在终端上。
    4. 先使用telnet来测试,然后编写客户端程序来测试。

    **官方文档:**https://studygolang.com/pkgdoc

    image-20231027170458418

    入门案例代码里的一些包,结构体,函数等提前预告:

    Listener是一个用于面向流的网络协议的公用的网络监听器接口。多个线程可能会同时调用一个Listener的方法。

    type Listener interface {
        // Addr返回该接口的网络地址
        Addr() Addr
        // Accept等待并返回下一个连接到该接口的连接
        Accept() (c Conn, err error)
        // Close关闭该接口,并使任何阻塞的Accept操作都会不再阻塞并返回错误。
        Close() error
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    **函数:**func Listen(net, laddr string) (Listener, error)

    **作用:**返回在一个本地网络地址laddr上监听的Listener。网络类型参数net必须是面向流的网络:

    “tcp”、“tcp4”、“tcp6”、“unix"或"unixpacket”。参见Dial函数获取laddr的语法。

    Conn接口代表通用的面向流的网络连接。多个线程可能会同时调用同一个Conn的方法。

    type Conn interface {
        // Read从连接中读取数据
        // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
        Read(b []byte) (n int, err error)
        // Write从连接中写入数据
        // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
        Write(b []byte) (n int, err error)
        // Close方法关闭该连接
        // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误
        Close() error
        // 返回本地网络地址
        LocalAddr() Addr
        // 返回远端网络地址
        RemoteAddr() Addr
        // 设定该连接的读写deadline,等价于同时调用SetReadDeadline和SetWriteDeadline
        // deadline是一个绝对时间,超过该时间后I/O操作就会直接因超时失败返回而不会阻塞
        // deadline对之后的所有I/O操作都起效,而不仅仅是下一次的读或写操作
        // 参数t为零值表示不设置期限
        SetDeadline(t time.Time) error
        // 设定该连接的读操作deadline,参数t为零值表示不设置期限
        SetReadDeadline(t time.Time) error
        // 设定该连接的写操作deadline,参数t为零值表示不设置期限
        // 即使写入超时,返回值n也可能>0,说明成功写入了部分数据
        SetWriteDeadline(t time.Time) error
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    **函数:**func Dial(network, address string) (Conn, error)

    **作用:**在网络network上连接地址address,并返回一个Conn接口。可用的网络类型有:

    “tcp”、“tcp4”、“tcp6”、“udp”、“udp4”、“udp6”、“ip”、“ip4”、“ip6”、“unix”、“unixgram”、“unixpacket”

    对TCP和UDP网络,地址格式是host:port或[host]:port,参见函数JoinHostPort和SplitHostPort。

    2.3 服务器监听

    我们想写一个服务器来监听

    创建一个server.go文件,作为服务器,简单写一下服务器的监听功能

    func main() {
    
        fmt.Println("服务器开始监听。。。")
        // net.Listen("tcp", "0.0.0.0:8888")
        // 1. tcp 表示使用网络协议是tcp
        // 2. 0.0.0.0:8888 表示在本地监听 8888端口
        listen, err := net.Listen("tcp", "0.0.0.0:8888")
        if err != nil {
           fmt.Println("listen err=", err)
           return
        }
        defer listen.Close() // 延时关闭listen
    
        // 循环等带客户端来连接我
        for {
           // 等待客户端连接
           fmt.Println("等待客户端来连接。。。")
           // Accept等待并返回下一个链接到该接口的连接
           conn, err := listen.Accept()
           if err != nil {
              // 连接失败
              fmt.Println("Accept() err=", err)
           } else {
              // 连接成功
              fmt.Printf("Accept() suc con=%v\n", conn)
           }
           // 这里准备起一个协程,为客户端服务
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    1. 使用net.Listen()函数创建一个监听器,监听0.0.0.0:8888地址。错误处理会打印出错误信息并返回。

    2. 使用defer关键字延迟关闭监听器。**注意:**一定要关闭,不然会一直占用资源。

    3. 进入无限循环,等待客户端的连接。在每次循环中,使用listen.Accept()函数接受客户端的连接请求。如果连接成功,则会打印连接信息。如果出现错误,则会打印错误信息。

    测试一下:

    使用telnet,可以测试这个服务器能不能监听,telnet需要下载,网上有教程,去搜搜

    我们运行这个server.go,下面会显示:

    服务器开始监听。。。
    等待客户端来连接。。。
    
    • 1
    • 2

    然后,cmd,打开命令行,输入:telnet 127.0.0.1 8888,就会进入telnet,然后我们看一下server.go

    image-20231027173038415

    从命令行就可以看出,连接成功过~~

    2.4 服务器接受客户端消息

    上一节,我们测试是否能够连接,下面我们写个客户端发送消息给服务器

    创建client文件夹作为客户端,然后创建client.go文件

    func main() {
        conn, err := net.Dial("tcp", "127.0.0.1:8888")
        if err != nil {
           fmt.Println("client dial err=", err)
           return 
        }
        //功能一:客户端可以发送单行数据,然后就退出
        reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]
    
        for {
    
           //从终端读取一行用户输入,并准备发送给服务器
           line, err := reader.ReadString('\n')
           if err != nil {
              fmt.Println("readString err=", err)
           }
           //如果用户输入的是 exit就退出
           line = strings.Trim(line, " \r\n")
           if line == "exit" {
              fmt.Println("客户端退出..")
              break
           }
    
           //再将line 发送给 服务器
           _, err = conn.Write([]byte(line + "\n"))
           if err != nil {
              fmt.Println("conn.Write err=", err)    
           }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    然后客户端写完之后,我们去server里写协程:

    func process(conn net.Conn) {
    
        //这里我们循环的接收客户端发送的数据
        defer conn.Close() //关闭conn
    
        for {
           //创建一个新的切片
           buf := make([]byte, 1024)
           //conn.Read(buf)
           //1. 等待客户端通过conn发送信息
           //2. 如果客户端没有wrtie[发送],那么协程就阻塞在这里
           //fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
           n, err := conn.Read(buf) //从conn读取
           if err != nil {
    
              fmt.Printf("客户端退出 err=%v", err)
              return //!!!
           }
           //3. 显示客户端发送的内容到服务器的终端
           fmt.Print(string(buf[:n]))
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这样就写好啦!!~~

    下一步,我们先运行server,再运行client,测试一下~

    image-20231027174118616

    然后,我们下面client输入hello world

    image-20231027174232077

    并且还能退出,在终端输入exit退出

    image-20231027174340002

    完美~~~~

    这样就Go学习基础快速入门就基本完成啦!!!!后面将会学习与MySQL的连接和Redis的连接,冲冲冲!!!

  • 相关阅读:
    【uniapp】【微信小程序】wxml-to-canvas
    API 网关的功能
    VFP常用命令、函数、属性、事件和方法
    U-NET总结
    机器学习第八次课
    设计师首选:最佳的5款网页设计软件
    SQL 基础入门教程
    社交创新的先锋:探秘Facebook背后的故事与智慧
    C2_W3_Assignment_吴恩达_中英_Pytorch
    冬季寒冷,普通空调如何做到智能控制,增温又降耗的?
  • 原文地址:https://blog.csdn.net/Hai_Helloyou/article/details/134081349