Go语言基础(五)反射、网络编程、pkg、单元测试

一、反射

1.1 introduction

Go的变量分两部分:类型信息+值信息

  • 类型信息是预先定义好的元信息
  • 程序执行中可变

Go代码编译后,变量变成了内存地址,程序是没法获取自身信息的,那我就是想要,怎么办?反射就是一种能获取到运行时信息的技术,比如字段名称、类型信息、结构体信息,etc。

Java等静态语言中也有反射,作用大同小异。

反射一个典型的用途是:json的反序列化

func foo() {
	// 这是一个字符串
	var s = `{"name":"amy","age":11}`
	// 这一个结构体
	type Person struct {
		Name string `json:"name"`	//注意,这里必须是Name,不能是 name ;否则json包是获取不到这个name信息的
		Age  int    `json:"age"`
	}
	// 我们知道,可以将json字符串转换成 结构体
	// 那到底是咋做到的?其实是 【反射】。go 会根据
	// 结构体的字段名,从json中找字段名,以及字段名
	// 对应的value
	var p Person
	// Unmarshal([]byte ,interface{}) ,在编译的时候是不知道 第二个参数的类型的,
	//只有在执行的时候才知道 ,这个类型信息是【反射】获取 的
	err := json.Unmarshal([]byte(s), &p)
	if err != nil {
		fmt.Println("failed to unmarshal:", err)
		return
	}
	fmt.Println(p)
}

1.2 Reflect

任何接口值都是由一个具体类型+具体类型的值 两部分组成。任意接口值,在反射中,都由reflect.Type 和 reflect.Value两部分组成,刚好对应起来了。

1.2.1 typeOf

reflect.TypeOf() 可获取任意值的类型对象,通过类型对象能访问任意值的类型信息。

1.2.2 type & kind

type:类型信息;
kind:我们可以使用type关键字创建自定义类型,而kind就是底层的类型,在反射中,若需要区分指针、结构体等时,就会用到 kind 。

Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的type.Name()都是返回空.

Go的reflect包里对 kind 做了列举,chan slice map 等都是原生的kind。
举个栗子:

func reflectType(i interface{}) {
	t := reflect.TypeOf(i)
	fmt.Printf("%v type:%v  kind:%v \n", t, t.Name(),t.Kind())
}

func foo() {

	var b = int32(1)
	reflectType(b)

	var f = func() {}
	reflectType(f)

	// 结构体【果然 GO也有内部的结构体】
	type person struct{
		age int
	}

	var p = person{10}
	reflectType(p)		// main.person type:person  kind:struct

	var s = make([]string,1)
	reflectType(s)		//  []string type:  kind:slice

	var m = make(map[string]int,1)
	reflectType(m)		// map[string]int type:  kind:map
}

1.2.3 reflect.ValueOf()

通过这个API能拿到接口的值

1.2.4 通过反射设置变量的值

函数传参传值时,是没法修改变量的值,只要传递指针才能修改变量值。反射中使用Elem()方法获取指针对应的值。


func foo() {
	var a = int64(10)
	fmt.Println("before altering:", a)
	r := reflect.ValueOf(&a)	// 注意:这里必须要传入 a 的指针,而不是 a ,否则即使 Elem().setInt() 也会panic
	if r.Kind() == reflect.Int64 {
		// r.SetInt(20) ``panic: reflect: reflect.Value.SetInt using unaddressable value
	}

	if r.Elem().Kind() == reflect.Int64 {
		r.Elem().SetInt(20)
	}

	fmt.Println("after altering:", a)
}

1.2.5 isNil isValid

IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、map、指针、切片之一;否则IsNil函数会导致panic。

IsValid()返回v是否持有一个值。如果v是Value零值会返回false,此时v除了IsValid、String、Kind之外的方法都会导致panic。

这两个的用途:
IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。

举个栗子:

func foo(){
	var a *int
	fmt.Println(reflect.ValueOf(a).IsNil())	
	fmt.Println(reflect.ValueOf(a).IsValid())

	// fmt.Println(reflect.ValueOf(nil).IsNil())	// panic: reflect: call of reflect.Value.IsNil on zero Value 
	fmt.Println(reflect.ValueOf(nil).IsValid())	//false 

	b:= struct{}{} // 匿名结构体
	fmt.Println(reflect.ValueOf(b).FieldByName("name").IsValid())

	m:= map[string]int{}
	// 看这个 map 中是否有 某一个名字叫key 的K
	fmt.Println(reflect.ValueOf(m).MapIndex(reflect.ValueOf("key")).IsValid())
}

1.3 结构体反射

任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()Field()方法获得结构体成员的详细信息。

1.4 反射的弊端

  • 反射似乎很强,但是反射代码可维护性极差
  • 反射性能很差
  • 反射中的类型错误,在编译时不会爆出来,在运行时才panic,分分钟P0故障,,,

二、网络编程

网络编程是个很大的范畴,这里只是简单涉及一下

2.0 协议

  • 网络分层
  • TCP/IP协议
  • HTTP等
    学好网络编程的关键并不是各种go网络框架,而是对协议本身的理解

2.1 tcp 黏包

先上个栗子:

服务端代码:

func main() {
	//启动一个服务端
	listner, err := net.Listen("tcp", "localhost:9091")
	if err != nil {
		fmt.Println("start error", err)
		return
	}

	for {
		conn, err := listner.Accept()
		if err != nil {
			fmt.Println("conn failed:", err)
			return
		}

		go process(conn)

		defer conn.Close()
	}

}


func process(conn net.Conn) {
	var buffer = make([]byte, 1024)
	for {
		num, err := conn.Read(buffer)
		if err == io.EOF {
			continue
		}
		if err != nil {
			fmt.Println("read failed", err)
			continue
		}

		// 看起来,这里应该输出 20 个 “hello there",实则不然
		// nagle的算法导致 实际上的TCP 请求并不是 20个
		fmt.Println(string(buffer[:num]))
	}
}

客户端代码:

func main() {
	// 启用一个客户端
	conn, err:=net.Dial("tcp", "localhost:9091")
	if err !=nil {
		fmt.Println("conn failed:",err)
		return
	}

	defer conn.Close()
  // 看起来,我们 发了 20个请求,那么服务端应该读到20次 
  // 但黏包、拆包的存在,导致了服务端读取次数实际少于20
	for i := 0; i < 20; i++ {
		_,err:=conn.Write([]byte("hello,there"))
		if err!=nil {
			fmt.Println("failed to write:", err)
			continue
		}
		
	}

}

Q:为啥 会出现黏包呢?

TCP是面向连接的协议,TCP通信是以流的形式。
黏包会出现在服务端,也会出现在客户端:

  • 出现在服务端:TCP收到包,会缓冲起来通知应用层过来处理,如果处理不及时,OS就会取到“黏”到一起的内容(几段数据)
  • 出现在客户端:nagle的算法改善了网络传输的效率,同时也带来了黏包的副作用。一言以蔽之:每次请求的内容并不是立即发送给服务端的,而是缓冲一会,看看后面还有没有请求可以一起发。

Q:如何解决黏包?
黏包的关键在于:接收方不知道自己处理的包的长度。我们可以进行封包、拆包操作。我们可以自定义一个协议,每个TCP的包的包头定长,包头里有数据长度的变量。拆包的时候,根据包头长度和数据长度就能准确知道每个包的边界。

在这里插入图片描述
按照上述的分析,这里给出个 自定义传输协议的封包、解包方法:

// 编码
func Encode(msg string) ([]byte, error) {
	// 读取消息长度,转成 int32 【刚好 4个字节】
	var length = int32(len(msg))
	var pkg = new(bytes.Buffer)
	// 写入消息头 【这里简单处理:整个消息头就只存储了 消息实体的长度】
	err := binary.Write(pkg, binary.LittleEndian, length)
	if err != nil {
		fmt.Println("write error:", err)
		return nil, errors.New("write error")
	}

	// 写入消息实体
	e := binary.Write(pkg, binary.LittleEndian, []byte(msg))
	if err != nil {
		fmt.Println("write entity failed:", e)
		return nil, errors.New("write entity error")
	}

	return pkg.Bytes(), nil
}

// 解码
func Decode(reader *bufio.Reader) (string,error){
	// 读取消息的长度
	lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
	lengthBuff := bytes.NewBuffer(lengthByte)
	var length int32
	err := binary.Read(lengthBuff, binary.LittleEndian, &length) // 把 数据实体的长度 读出来赋给 length
	if err != nil {
		return "", err
	}
	// Buffered返回缓冲中现有的可读取的字节数。
	if int32(reader.Buffered()) < length+4 {
		return "", err
	}

	// 读取真正的消息数据
	pack := make([]byte, int(4+length))
	_, err = reader.Read(pack)
	if err != nil {
		return "", err
	}
	return string(pack[4:]), nil
}

三、网络库 net/http

3.1 客户端

  • 利用go 原生 net 库,发起一个get请求
  • 利用go原生net库,发起一个post
  • 自定义一个client【设置请求头、重定向策略等】
  • 自定义transport【管理代理、TLS配置、keep-alive、压缩配置等】

Client transport可以被多个go routine 复用,且是昂贵的对象,复用是best practice

3.2 服务端

go 的原生网络库十分强大,在兼顾性能的同时,提供了便捷易用的API。实现一个简单的服务器,只需要几行代码。举个栗子:

func main() {
	http.HandleFunc("/index",func(w http.ResponseWriter, r *http.Request){
		// w.Write([]byte("hello,there")) -->返回一个 hello,there的字符串
		content,err:=ioutil.ReadFile("./poem.txt")
		if err == nil {
			w.Write([]byte(content))
			return
		}
		fmt.Println(err)
		w.Write([]byte("error page"))
	})
	http.ListenAndServe("localhost:9091", nil)
}

四、pkg

4.1 pkg

go 中使用包pkg 来复用代码【几乎所有的语言都是如此】。
包,就是存放.go文件的目录:

  • 包名和目录名可以不一致;.go 文件第一行 package A,即声明这个 .go属于 A 包
  • main包是程序的入口包,编译后会得到一个可执行文件。

4.2 可见性

go 中使用 标识符(变量、常量、函数、结构体、结构体字段等)首字母大小写来标识是否对外可见 【就是说 这个标识符能不能被别的包引用到】

4.3 import

包名是从 $GOPATH/src/ 后开始计算的,使用/进行路径分隔。

4.4 init() 函数

var x int8 = 1

const pi= 3.1415

// init() 在go 中具有特殊含义,是 go 运行导入包语句
// 会自动 执行 init() .
// init() 不应该被显式地调用
func init(){
	fmt.Println(x)
}

func main(){
	fmt.Println("main starts")
}

init() 函数执行时机:

  • 全局声明 --> init() 函数 --> main()

4.5 包引用关系

.go文件从main包开始检查其导入的包,每个包可能又依赖了其他的包。go编程出一个树状的引用关系,再根据引用顺序决定编译顺序 ,依次编译这些包的代码。

运行时,最后被引用的包最先执行 init()函数。
在这里插入图片描述

五、测试 (TODO)

5.1 单元测试

5.2 测试组&子测试

5.3 基准测试

5.4 demo

六、性能优化(概述)

6.1 Go性能优化的几点

  • CPU profiling
  • 内存 profiling
  • Go routing profiling:报告go routing 使用情况,有哪些go routing,调用关系如何
  • Blocking profiling:分析go routing 不在运行的情况,分析查找死锁

跟Java中的 async-profiler 多像!

6.2 采集和分析性能数据

go 原生提供了性能工具:

  • runtime/pprof :采集工具型应用的运行时数据分析

  • net/http/pprof:采集服务型应用的运行时数据分析

  • pprof 提供了命令行工具查看性能数据

  • go torch 抓取火焰图

  • 压测工具wrk

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值