1.4.1 Go语言快速入门
学习Go语言的第一步当然是掌握所有的数据类型。Go语言本身提供了丰富的数据类型,如字符串、切片、散列表等,并允许开发者自定义结构体、接口等。
1.数据类型
图1-7列出了Go语言基本的数据类型(整数类型等过于简单,这里就不进行介绍了),以及每种数据类型需要开发者重点关注的知识点,如图1-7所示。
图1-7 Go语言基本数据类型
参考图1-7,接下来将按照顺时针方向逐个介绍每一种数据类型。
(1)函数
Go语言的函数与其他编程语言的非常类似,不同的是,Go语言函数可以返回多个值,而其他编程语言如C语言、PHP语言等只能返回一个值。另外,在函数传递参数时,一定要清楚Go语言函数传递的是值还是引用(Go语言函数传递的是值)。这两种传递参数的方式有什么区别呢?如果传递的是引用,那么函数内部对输入参数的修改会同步到调用方;如果传递的是值,那么函数内部对输入参数的修改将不会影响调用方。
(2)数组
数组是一种顺序存储的线性数据结构,我们可以按索引下标访问数组元素。在使用数组时,一定要注意避免索引越界,比如数组长度为3(最多能存储3个元素,索引下标只能是0、1、2),而你的Go程序访问的索引下标大于或等于3,这时候你的Go程序将会编译失败(因为数组长度是静态的,所以编译期间可以判断是否出现索引越界情况)。
如果你写过C语言,会知道当你将数组作为函数参数传递时,如果函数内部修改了数组元素,调用方也会同步修改,这是因为在C语言中数组作为函数参数时传递的是数组首地址。Go语言不同,当你将数组作为函数参数传递时,如果函数内部修改了数组元素,调用方并不会同步修改,这是因为Go语言会将所有数组的值复制一份,再作为输入参数传递。
(3)切片
切片是Go语言常用的数据类型之一,其本质是动态数组。切片有两个基本概念:长度与容量,长度是切片存储的元素数目,容量是切片存储的最大元素数目。切片同样可以按索引下标访问数据,但是需要注意避免索引越界,也就是说访问切片的索引下标必须小于切片的长度,否则会抛出panic异常。
切片支持for-range遍历语法,只是需要注意,通过该方式遍历获取到的数据,其实是一份数据副本,修改该数据并不会影响切片底层数组存储的数据。
切片本质上是通过预分配内存策略来提升效率的,当我们使用append函数向切片追加数据时,该函数内部会检测切片容量是否不够,如果容量不够则先触发扩容。扩容就是申请更大的内存作为底层数组,同时将所有数据复制到新数组。当切片容量比较小时,Go语言按照原始容量的2倍扩容;当切片容量比较大时,Go语言按照原始容量的25%扩容。
切片还支持截取操作,也就是截取切片的一部分数据作为新的切片,这一操作虽然简单,但是需要关注新切片的长度、容量。另外,修改新切片数据时是否会影响原始切片数据呢?
最后,当切片作为函数参数传递时,如果函数内部修改了切片(通过索引下标修改数据,或者追加数据),调用方会同步修改吗?如果追加数据导致切片触发扩容了,再通过索引下标修改数据,调用方会同步修改吗?
当然,当你学习了Go语言切片的底层数据结构之后,上面这些疑问将会迎刃而解,这里就不一一解释了。
(4)字符串
字符串比较简单,只需要重点了解一些常用的字符串库函数即可,另外可以适当了解一下Go语言字符串的底层数据结构,以及字符串编码。
(5)散列表map
map是Go语言常用的数据类型之一,用于存储键-值对。插入键-值对,以及根据键查找、修改、删除键-值对的时间复杂度都是O(1)。为什么map的增删改查效率能这么高呢?这依赖于map的底层数据结构。有兴趣的读者可以自行研究。
map同样支持for-range遍历语法,与切片的遍历类似,通过该方式遍历获取到的数据,其实是一份数据副本,修改该数据并不会影响map底层存储的数据。
另外,当我们一直向map插入键-值对时,同样有可能触发map的扩容操作。为什么呢?因为map通常是基于数组+链表方式实现的(首先根据键计算得到一个散列值,再映射到数组的某一索引位置。如果多个键映射到同一个数组索引,可通过链表串联),当键-值对数目过多时,链表的平均长度会增加,map的增删改查效率也会随之降低。这时候就需要扩容了,扩容就是申请更大的内存作为数组,并重新计算所有键-值对的散列值,映射到新的数组。
最后不得不提map的并发问题,各位读者一定要记得,多个协程并发访同一个map变量可能会抛出panic异常。
(6)结构体
Go语言支持面向对象编程,但是与传统的面向对象语言如C++、Java等略有不同。Go语言没有类的概念,只有结构体;结构体可以拥有属性,也可以拥有方法。我们可以通过结构体实现面向对象编程。
另外,面向对象有一个很重要的概念——继承,子类可以继承父类的某些属性或方法。Go语言结构体也支持“继承”,不过是通过组合的方式实现的继承。
(7)接口
Go语言支持面向接口编程,接口用于定义一组方法。与传统的面向对象语言如C++、Java等不同,Go语言结构体并不需要声明实现某个接口,只要结构体实现了该接口的所有方法,就认为其实现了该接口。
Go语言将接口分为两种:带方法的接口和不带方法的接口(空接口)。带方法的接口一般比较复杂,底层基于iface实现;不带方法的接口则基于eface实现。通常,当我们不知道变量的类型时,会将变量类型声明为空接口。任何类型的变量都能赋值给空接口类型的变量,反之则不行。将空接口类型转化为具体的类型需要使用类型断言,但是需要注意,如果类型断言使用不当,可能会抛出panic异常。
(8)反射
反射给Go语言带来了一些动态特性,它可以帮助我们获取类型(reflect.Type)的所有方法、属性等,也可以帮助我们获取变量(reflect.Value)的值以及类型,更新变量的值等。
(9)泛型
Go语言从1.18版本开始支持泛型,为什么需要泛型呢?假设有这么一个业务场景:我们需要实现一个函数,输入两个参数,函数返回它们相加的值,输入参数可以是两个整型(int)、浮点数(float),还有可能是字符串等。Go语言是强类型语言,任何变量或函数参数都需要定义明确的参数类型,所以针对这一场景我们只能定义多个函数。
泛型使得我们可以定义一个函数模板(类型不确定),等到真正调用函数的时候,再确定函数的参数以及返回值等具体类型。关于泛型的使用示例,可以参考Go语言官方提供的实验库(golang.org/x/exp)。
经过第一步的学习,我们对Go语言常用的数据类型有了一定的了解,但是这远远不够,因为实际项目开发往往依赖于一些标准库或开源组件。
2.标准库
接下来简单介绍一下Go语言提供的一些常用标准库,如图1-8所示。
图1-8 Go语言常用标准库
参考图1-8,接下来将按照顺时针方向逐个介绍主要的标准库。
(1)net/http.Server
Go语言创建HTTP服务还是非常方便的,基于标准库net/http.Server,只需要几行代码就能实现。因此,我们不应该局限于使用,还需要了解一下标准库net/http.Server的HTTP请求处理框架、请求处理器等。当然,也少不了WriteTimeout与IdleTimeout这两个配置。1.1节中提到,当请求处理时间超过WriteTimeout时,Go服务会关闭连接从而导致502异常状态码;IdleTimeout配置不合适时,也有可能会导致偶发性的502异常状态码。第12章将会整体介绍Go服务的几种502情况。
(2)net/http.Client
顾名思义,标准库net/http.Client用于发起HTTP请求。标准库net/http.Client的使用同样比较简单,但是你知道它是基于短连接还是长连接发起的HTTP请求吗?如果是长连接,会涉及连接池的概念,而且长连接如果使用不当还会引发偶现的“connection reset by peer”错误(第12章将会详细介绍)。
(3)context
标准库context表示上下文,它主要有两个核心功能:①在整个函数调用链传值;②超时控制。1.1.2小节就是基于context实现的HTTP请求超时控制。
(4)JSON
JSON是一种常用的数据传输协议,Go语言也提供了这种协议的序列化与反序列化标准库。不过在使用过程中需要注意,Go语言是强类型语言,比如字符串、整数是两种不同的类型。在JSON格式中,整数没有双引号,字符串是有双引号的。如果混用这两种类型,可能会导致JSON反序列化出错(类型不匹配错误)。
另外,Go语言也允许开发者自定义序列化与反序列化方式,只需要实现json.Marshaler与json.Unmarshaler这两个接口即可。
(5)单元测试
在实际项目的开发过程中,当然少不了单元测试,Go语言为我们提供了完善的单元测试库,基于该库可以实现基础测试、性能测试、代码覆盖率测试等。
(6)其他库
还有其他一些标准库,比如net/rpc、系统库os、数学库math等,这里就不一一介绍了,Go语言对此有详细的注释说明,使用时查阅即可。
通过前面的学习,我们基本上能完成一些实际项目中的开发任务了,但是仅限于简单的增删改查等。这是因为,关于Go语言的核心——并发,我们还不了解。Go语言的生态系统,如常用的开源组件等,也不了解。当遇到线上问题时,我们也束手无策。关于后续两个阶段的学习路线,我们将在后面两个小节介绍。