Skip to content

Latest commit

 

History

History
502 lines (320 loc) · 30.5 KB

Go语言从入门到进阶实战(视频教学版).md

File metadata and controls

502 lines (320 loc) · 30.5 KB

Go语言从入门到进阶实战(视频教学版)

徐波

前言

Go语言被称为“互联网时代的C语言”。互联网的短、频、快特性在Go语言中体现得淋漓尽致。一个熟练的开发者只需要短短的一周时间就可以从学习阶段转到开发阶段,并完成一个高并发的服务器开发。

本书内容

第10章 反射 本章按照反射的类别分为两部分:反射类型对象(reflect.Type)和反射值对象(reflect.Value)。首先介绍了反射类型的获取及遍历方法,同时介绍了反射类型对象获取结构体标签的方法;接着介绍了反射值对象获取及修改值和遍历值等;最后通过使用反射将结构体串行化为JSON格式字符串的示例,介绍了反射在实际中的运用。

1.1 Go语言特性

Go语言是Google公司开发的一种静态型、编译型并自带垃圾回收和并发的编程语言。

Go语言在语言层可以通过goroutine对函数实现并发执行。goroutine类似于线程但是并非线程,goroutine会在Go语言运行时进行自动调度。因此,Go语言非常适合用于高并发网络服务的编写。

Go语言这种从零开始使用到解决问题的速度,在其他语言中是完全不可想象的。

Go语言的代码可以直接输出为目标平台的原生可执行文件。Go语言不使用虚拟机,只有运行时(runtime)提供垃圾回收和goroutine调度等。Go语言使用自己的链接器,不依赖任何系统提供的编译器、链接器。因此编译出的可执行文件可以直接运行在几乎所有的操作系统和环境中。

3.工程结构简单 Go语言的源码无须头文件,编译的文件都来自于后缀名为go的源码文件;Go语言无须解决方案、工程文件和Make File。只要将工程文件按照GOPATH的规则进行填充,即可使用go build/go install进行编译,编译安装的二进制可执行文件统一放在bin文件夹下。

6.原生支持并发 Go语言的特性就是从语言层原生支持并发,无须第三方库、开发者的编程技巧及开发经验就可以轻松地在Go语言运行时来帮助开发者决定怎么使用CPU资源。

多个goroutine中,Go语言使用通道(channel)进行通信,程序可以将需要并发的程序设计为生产者和消费者的模式,将数据放入通道。通道的另外一端的代码将这些数据进行并发计算并返回结果,如图1-1所示。

7.性能分析 安装Go语言开发包后,使用Go语言工具链可以直接进行Go代码的性能分析。Go的性能分析工具将性能数据以二进制文件输出,配合Graphviz即可将性能分析数据以图形化的方式展现出来,如图1-2所示。

for两边的括号被去掉,int声明被简化为“:=”,直接通过编译器右值推导获得a的变量类型并声明。

(3)强制的代码风格 Go语言中,左括号必须紧接着语句不换行。

在Go语言中,自增操作符不再是一个操作符,而是一个语句。因此,在Go语言中自增只有一种写法:  i++ 如果写成前置自增“++i”,或者赋值后自增“a=i++”都将导致编译错误。

1.2 使用Go语言的项目

Go语言的简单、高效、并发特性吸引了众多传统语言开发者的加入,而且人数越来越多。

1.4 搭建开发环境

GOPATH是Go语言编译时参考的工作路径

2.1 变量

Go语言作为C语言家族的新派代表,在C语言的定义方法和类型上做了优化和调整,更加灵活易学。

2.1.2 初始化变量 Go语言在声明变量时,自动对变量对应的内存区域进行初始化操作。每个变量会初始化其类型的默认值,例如: ● 整型和浮点型变量的默认值为0。 ● 字符串变量的默认值为空字符串。 ● 布尔型变量默认为bool。 ● 切片、函数、指针变量的默认为nil。

标准格式 var变量名 类型 = 表达式 例如:游戏中,玩家的血量初始值为100。可以这样写: var hp int = 100 这句代码中,hp为变量名,类型为int,hp的初始值为100。

3.短变量声明并初始化 var的变量声明还有一种更为精简的写法,例如: hp := 100 这是Go语言的推导声明写法,编译器会自动根据右值类型推断出左值的对应类型。 注意:由于使用了“:=”,而不是赋值的“=”,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。

2.2 数据类型

自动匹配平台的int和uint Go语言也有自动匹配特定平台整型长度的类型——int和uint。 可以跨平台的编程语言可以运行在多种平台上。平台的字节长度是有差异的。64位平台现今已经较为普及,但8位、16位、32位的操作系统依旧存在。16位平台上依然可以使用64位的变量,但运行性能和内存性能上较差。同理,在64位平台上大量使用8位、16位等与平台位数不等长的变量时,编译器也是尽量将内存对齐以获得最好的性能。 不能正确匹配平台字节长度的程序就类似于用轿车运一头牛和用一辆卡车运送一头牛的情形一样。 在使用int和uint类型时,不能假定它是32位或64位的整型,而是考虑int和uint可能在不同平台上的差异。

反之,在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用int和uint。

布尔型无法参与数值运算,也无法与其他类型进行转换。

在源码中,将字符串的值以双引号书写的方式是字符串的常见表达方式,被称为字符串字面量(string literal)。这种双引号字面量不能跨行。如果需要在源码中嵌入一个多行字符串时,就必须使用“'”字符,代码如下:  const str = ' 第一行 第二行 第三行 \r\n ' fmt.Println(str)

2.6 字符串应用

这里的差异是由于Go语言的字符串都以UTF-8格式保存,每个中文占用3个字节,因此使用len()获得两个中文文字对应的6个字节。 如果希望按习惯上的字符个数来计算,就需要使用Go语言中UTF-8包提供的Rune Count In String()函数,统计Uncode字符数量。

2.8 类型别名(Type Alias)

类型别名规定:Type Alias只是Type的别名,本质上Type Alias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,

类型别名与类型定义表面上看只有一个等号的差异,那么它们之间实际的区别有哪些呢?

编译器提示:不能在一个非本地的类型time.Duration上定义新方法。非本地方法指的就是使用time.Duration的代码所在的包,也就是main包。因为time.Duration是在time包中定义的,在main包中使用。time.Duration包与main包不在同一个包中,因此不能为不在一个包中的类型定义方法。解决这个问题有下面两种方法:

3.1 数组——固定大小的连续空间

数组是一段固定长度的连续内存区域。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。 提示:C语言和Go语言中的数组概念完全一致。C语言的数组也是一段固定长度的内存区域,数组的大小在声明时固定下来。下面演示一段C语言的数组:

var team = [...]string{"hammer", "soldier", "mum"} “...”表示让编译器确定数组大小。上面例子中,编译器会自动为这个数组设置元素个数为3。

3.2 切片(slice)——动态分配大小的连续空间

3.2.1 从数组或切片生成新的切片 切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。 从连续内存区域生成切片是常见的操作,格式如下:  slice [开始位置:结束位置] ● slice表示目标切片对象。 ● 开始位置对应目标切片对象的索引。

a是一个拥有3个整型元素的数组,被初始化数值1到3。使用a[1:2]可以生成一个新的切片。代码运行结果如下:[1 2 3] [2]

.从指定范围中生成切片 切片和数组密不可分。如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者。出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片。

3.2.3 使用make()函数构造切片 如果需要动态地创建一个切片,可以使用make()内建函数,格式如下:  make( []T, size, cap ) ● T:切片的元素类型。 ● size:就是为这个类型分配多少个元素。 ● cap:预分配的元素数量,这个值设定后不影响size,只是能提前分配空间,降低多次分配空间造成的性能问题。

Go语言的内建函数append()可以为切片动态添加元素。每个切片会指向一片内存空间,这片空间能容纳一定数量的元素。当空间不能容纳足够多的元素时,切片就会进行“扩容”。“扩容”操作往往发生在append()函数调用时。 切片在扩容时,容量的扩展规律按容量的2倍数扩充,例如1、2、4、8、16……,代码如下:

提示:往一个切片中不断添加元素的过程,类似于公司搬家。公司发展初期,资金紧张,人员很少,所以只需要很小的房间即可容纳所有的员工。随着业务的拓展和收入的增加就需要扩充工位,但是办公地的大小是固定的,无法改变。因此公司选择搬家,每次搬家就需要将所有的人员转移到新的办公点。

3.2.5 复制切片元素到另一个切片 使用Go语言内建的copy()函数,可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:  copy( dest Slice, src Slice []T) int ● src Slice为数据来源切片。 ● dest Slice为复制的目标。目标切片必须分配过空间且足够承载复制的元素个数。来源和目标的类型一致,copy的返回值表示实际发生复制的元素个数。

Go语言中切片删除元素的本质是:以被删除元素为分界点,将前后两个部分的内存重新连接起来。

提示:Go语言中切片元素的删除过程并没有提供任何的语法糖或者方法封装,无论是初学者学习,还是实际使用都是极为麻烦的。 连续容器的元素删除无论是在任何语言中,都要将删除点前后的元素移动到新的位置。随着元素的增加,这个过程将会变得极为耗时。因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高,就需要反思是否需要更换其他的容器(如双链表等能快速从删除点删除元素)。

5.6 闭包(Closure)——引用了外部变量的匿名函数

函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”。函数是编译期静态的概念,而闭包是运行期动态的概念

闭包对环境中变量的引用过程,也可以被称为“捕获”,

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。

5.8 延迟执行语句(defer)

defer归属的函数即将返回时,将延迟处理的语句按defer的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

延迟调用是在defer所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。

在下面的例子中会在函数中并发使用map,为防止竞态问题,使用sync.Mutex进行加锁

map默认不是并发安全的,准备一个sync.Mutex互斥量保护map的访问。

文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源。

为了释放资源,必须要调用f的Close()方法来关闭文件,否则会发生资源泄露。

5.9 处理运行时发生的错误

● 在函数调用后需要检查错误,如果发生错误,进行必要的错误处理。

Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数。同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。

net.Dial()是Go语言系统包net即中的一个函数,一般用于创建一个Socket连接。 net.Dial拥有两个返回值,即Conn和error。这个函数是阻塞的,因此在Socket操作后,会返回Conn连接对象和error;如果发生错误,error会告知错误的类型,Conn会返回空。

error是Go系统声明的接口类型,代码如下:  type error interface { Error() string }

所有符合Error() string格式的方法,都能实现错误接口。

在Go语言中,使用errors包进行错误的定义,格式如下:  var err = errors.New("this is an error") 错误字符串由于相对固定,一般在包作用域声明,应尽量减少在使用时直接使用errors.New返回。

1.errors包 Go语言的errors中对New的定义非常简单,代码如下:  01 // 创建错误对象 02 func New(text string) error { 03 return &error String{text} 04 }

5.10 宕机(panic)——程序终止运行

Go语言程序在宕机时,会将堆栈和goroutine信息输出到控制台,所以宕机也可以方便地知晓发生错误的位置。如果在编译时加入的调试信息甚至连崩溃现场的变量值、运行状态都可以获取,那么如何触发宕机呢?例如下面的代码:  package main func main() { panic("crash") }

代码运行崩溃并输出如下:  panic: crash goroutine 1 [running]: main.main() F:/src/tester/main.go:5 +0x6b

panic()的参数可以是任意类型,后文将提到的recover参数会接收从panic()中发出的内容。

手动宕机进行报错的方式不是一种偷懒的方式,反而能迅速报错,终止程序继续运行,防止更大的错误产生。不过,如果任何错误都使用宕机处理,也不是一种良好的设计。因此应根据需要来决定是否使用宕机进行报错。

5.10.3 在宕机时触发延迟执行语句 当panic()触发的宕机发生时,panic()后面的代码将不会被运行,但是在panic()函数前面已经运行过的defer语句依然会在宕机发生时发生作用

5.11 宕机恢复(recover)——防止程序崩溃

5.11 宕机恢复(recover)——防止程序崩溃 无论是代码运行错误由Runtime层抛出的panic崩溃,还是主动触发的panic崩溃,都可以配合defer和recover实现错误捕捉和恢复,让代码在发生崩溃后允许继续运行。

Go没有异常系统,其使用panic触发宕机类似于其他语言的抛出异常,那么recover的宕机恢复机制就对应try/catch机制。

第6章 结构体(struct)

Go语言的结构体与“类”都是复合结构体,但Go语言中结构体的内嵌配合接口比面向对象具有更高的扩展性和灵活性。 Go语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法。

6.2 实例化结构体——为结构体分配内存并初始化

6.2 实例化结构体——为结构体分配内存并初始化 结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存。因此必须在定义结构体并实例化后才能使用结构体的字段。

基本实例化格式如下:  var ins T ● T为结构体类型。

使用“.”来访问结构体的成员变量,如p.X和p.Y等。结构体成员变量的赋值方法与普通变量一致。

6.2.2 创建指针类型的结构体 Go语言中,还可以使用new关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

● T为类型,可以是结构体、整型、字符串等。 ● ins:T类型被实例化后保存到ins变量中,ins的类型为*T,属于指针。

Go语言让我们可以像访问普通结构体一样使用“.”访问结构体指针的成员。

6.2.3 取结构体的地址实例化 在Go语言中,对结构体进行“&”取地址操作时,视为对该类型进行一次new的实例化操作。取地址格式如下:  ins := &T{}

● T表示结构体类型。 ● ins为结构体的实例,类型为*T,是指针类型。

● 第3行,命令绑定的变量,使用整型指针绑定一个指针。指令的值可以与绑定的值随时保持同步。

● 第9行,对结构体取地址实例化。

取地址实例化是最广泛的一种结构体实例化方式。可以使用函数封装上面的初始化过程,代码如下:

6.3 初始化结构体的成员变量

● 第6行,relation由People类型取地址后,形成类型为*People的实例。

结构体成员中只能包含结构体的指针类型,包含非指针类型会引起编译错误。

6.5 方法

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收器(Receiver)。

在Go语言中,接收器的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法

在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器。大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。

第7章 接口(interface)

接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程。

Go语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。

非侵入式设计是Go语言设计师经过多年的大项目经验总结出来的设计之道。只有让接口和实现者真正解耦,编译速度才能真正提高,项目之间的耦合度也会降低不少。

7.1 声明接口

接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

每个接口类型由数个方法组成。接口的形式代码如下:  type接口类型名interface{ 方法名1( 参数列表1 )

● 接口类型名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer,有关闭功能的接口叫Closer等

● 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。 ● 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略

这个接口可以调用Write()方法写入一个字节数组([]byte),返回值告知写入字节数(n int)和可能发生的错误(err error)。

Go语言的每个接口中的方法数量不会很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。

7.2 实现接口的条件

在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。

7.5 示例:使用接口进行数据的排序

从Go 1.8开始,Go语言在sort包中提供了sort.Slice()函数进行更为简便的排序方法。sort.Slice()函数只要求传入需要排序的数据,以及一个排序时对元素的回调函数,类型为func(i,j int) bool,sort.Slice()函数的定义如下:  func Slice(slice interface{}, less func(i, j int) bool)

8.1 工作目录(GOPATH)

)。 在Go 1.8版本之前,GOPATH环境变量默认是空的。从Go 1.8版本开始,Go开发包在安装完成后,将GOPATH赋予了一个默认的目录

在GOPATH指定的工作目录下,代码总是会保存在$GOPATH/src目录下。在工程经过go build、go install或go get等指令后,会将产生的二进制可执行文件放在$GOPATH/bin目录下,生成的中间缓存文件会被保存在$GOPATH/pkg下。

这种设置全局GOPATH的方法可能会导致当前项目错误引用了其他目录的Go源码文件从而造成编译输出错误的版本或编译报出一些无法理解的错误提示。

因此,建议大家无论是使用命令行或者使用集成开发环境编译Go源码时,GOPATH跟随项目设定。

建议开发者不要设置全局的GOPATH,而是随项目设置GOPATH。

8.4 导入包(import)——在代码中使用其他的代码

8.4 导入包(import)——在代码中使用其他的代码 要引用其他包的标识符,可以使用import关键字,导入的包名使用双引号包围,包名是从GOPATH开始计算的路径,使用“/”进行路径分隔。

● 第4行,导入chapter08/importadd/mylib包。 ● 第9行,使用mylib作为包名,并引用Add()函数调用。

1 export GOPATH=/home/davy/golangbook/code 02 go install chapter08/importadd

● 第1行,根据你的GOPATH不同,设置GOPATH。 ● 第2行,使用go install指令编译并安装chapter08/code8-1到GOPATH的bin目录下。

8.4.3 匿名导入包——只导入包但不使用包内类型和数值 如果只希望导入包,而不使用任何包内的结构和类型,也不调用包内的任何函数时,可以使用匿名导入包

第9章 并发

goroutine是由Go语言的运行时调度完成,而线程是由操作系统调度完成。

Go语言还提供channel在多个goroutine间进行通信。goroutine和channel是Go语言秉承的CSP(Communicating Sequential Process)并发模式的重要实现基础。

9.1 轻量级线程(goroutine)——根据需要随时创建的“线程”

go func( 参数列表 ){ 函数体 }( 调用参数列表 )

● 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。 ● 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。 两个概念的区别是:任务是否同时执行。举一个生活中的例子:打电话和吃饭。 吃饭时,电话来了,需要停止吃饭去接电话。电话接完后回来继续吃饭,这个过程是并发执行。 吃饭时,电话来了,边吃饭边接电话。这个过程是并行执行。

goroutine间使用channel通信;coroutine使用yield和resume操作。 goroutine和coroutine的概念和运行机制都是脱胎于早期的操作系统。 coroutine的运行机制属于协作式任务处理,早期的操作系统要求每一个应用必须遵守操作系统的任务处理规则,应用程序在不需要使用CPU时,会主动交出CPU使用权。如果开发者无意间或者故意让应用程序长时间占用CPU,操作系统也无能为力,表现出来的效果就是计算机很容易失去响应或者死机。

9.4 同步——保证并发环境下数据访问的正确性

9.4.2 互斥锁(sync.Mutex)——保证同时只有一个goroutine可以访问共享资源 互斥锁是一种常用的控制共享资源访问的方法。

—在读比写多的环境下比互斥锁更高效 在读多写少的环境中,可以优先使用读写互斥锁,sync包中的RWMutex提供了读写互斥锁的封装。

如果此时另外一个goroutine并发访问了count Guard,同时也调用了count Guard.RLock()时,并不会发生阻塞。

4 等待组(sync.Wait Group)——保证在并发环境中完成指定数量的任务 除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步。

等待组内部拥有一个计数器,计数器的值可以通过方法调用实现计数器的增加和减少。

1 // 声明一个等待组 12 var wg sync.Wait Group 13

// 每一个任务开始时,将等待组增加1 25 wg.Add(1)

             // 使用defer,表示函数完成时将等待组值减1

31 defer wg.Done()

3 // 等待所有的任务完成 44 wg.Wait() 45

● 第31行,在匿名函数结束时会执行这一句以表示任务完成。wg.Done()方法等效于执行wg.Add(-1)。 ● 第34行,使用http包提供的Get()函数对url进行访问,Get()函数会一直阻塞直到网站响应或者超时。

● 第40行,这里将url通过goroutine的参数进行传递,是为了避免url变量通过闭包放入匿名函数后又被修改的问题。

第10章 反射

反射是指在程序运行期对程序本身进行访问和修改的能力。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。 Go程序在运行期使用reflect包访问程序的反射信息。

10.1 反射的类型对象(reflect.Type)

使用reflect.Type Of()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。

通过reflect.Type Of()取得变量a的类型对象type Of A,类型为reflect.Type()。 ● 第14行中,通过type Of A类型对象的成员函数,可以分别获取到type Of A变量的类型名为int,种类(Kind)为int。

Go程序中的类型(Type)指的是系统原生数据类型,如int、string、bool、float32等类型,以及使用type关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用type A struct{}定义结构体时,A就是struct{}的类型。

种类(Kind)指的是对象归属的品种,在reflect包中有如下定义:  type Kind uint const ( Invalid Kind = iota // 非法类型 Bool // 布尔型 Int // 有符号整型 Int8 // 有符号8位整型

10.1.2 指针与指针指向的元素 Go程序中对指针获取反射对象时,可以通过reflect.Elem()方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个“*”操作

reflect.Type的Field()方法返回Struct Field结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(Struct Tag)等

Tag Struct Tag // 字段的结构体标签

10.1.4 结构体标签(Struct Tag)——对结构体字段的额外信息标签 通过reflect.Type获取结构体成员信息reflect.Struct Field结构中的Tag被称为结构体标签(Struct Tag)。

JSON、BSON等格式进行序列化及对象关系映射(Object Relational Mapping,简称ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。

3.结构体标签格式错误导致的问题 编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误

第12行中,在json:和"type"之间增加了一个空格。这种写法没有遵守结构体标签的规则,因此无法通过Tag.Get获取到正确的json对应的值。 这个错误在开发中非常容易被疏忽,造成难以察觉的错误。

10.2 反射的值对象(reflect.Value)

10.2 反射的值对象(reflect.Value) 反射不仅可以获取值的类型信息,还可以动态地获取或者设置变量的值。Go语言中使用reflect.Value获取和设置变量的值。

11.2 编译后运行(go run)

go run命令会编译源码,并且直接执行源码的main()函数,不会在当前目录留下可执行文件。

11.3 编译并安装(go install)

go install的功能和go build类似,附加参数绝大多数都可以与go build通用。go install只是将编译的中间文件放在GOPATH的pkg目录下,以及固定地将编译结果放在GOPATH的bin目录下。

11.5 测试(go test)

Go语言拥有一套单元测试和性能测试系统,仅需要添加很少的代码就可以快速测试一段需求代码。

要开始一个单元测试,需要准备一个go源码文件,在命名文件时需要让文件必须以_test结尾

12.5 优雅地处理TCP粘包

发送封包时,需要将封包的Size字段从无符号十六位整型转换为字节数组,这里需要用到binary包的Write()函数,该函数可以根据数据的格式和需要的大小端,将数据写入io.Writer接口。

大小端也是发送和接收时需要处理的细节。大小端是数据按二进制存储时,高低位的存储顺序。如现在的英特尔的80x86芯片及兼容芯片都是使用小端格式。手机里的ARM芯片默认采用小端。而Java由于平台无关,默认都是大端。网络传输的数据普遍采用大端,但网络传输只是底层协议,与用户层无关。

bytes.Buffer是一个自动增长的缓冲,实现了io.Writer接口,因此binary.Write()函数可以将数据直接写入bytes.Buffer。下面代码将需要发送的Packet的所有字段写入bytes.Buffer后,再一次性将bytes.Buffer的数据写入Socket对应的data Writer。

// 准备一个字节数组缓冲 19 var buffer bytes.Buffer 20 21 // 将Size写入缓冲 22 if err := binary.Write(&buffer, binary.Little Endian, uint16(len (data))); e

第22行,使用binary.Write,将以uint16格式传入的切片大小,以小端方式写入bytes.Buffer中。 ● 第27行,将包体内存写入到bytes.Buffer中。 ● 第32行,将buffer中刚才写入的数据取出,保存到out中。 ● 第35行,将out中的数据写入data Writer接口对应的Socket中