一、反射
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