参考文档
[1]: Go 语言官方文档
[2]: Go 入门指南
[3]: Go web编程
Go语言介绍
- Go语言起源
Go 语言起源 2007 年,并于 2009 年正式对外发布。它从 2009 年 9 月 21 日开始作为谷歌公司 20% 兼职项目,即相关员工利用 20% 的空余时间来参与 Go 语言的研发工作。该项目的三位领导者均是著名的计算机工程师:Robert Griesemer,参与开发 Java HotSpot 虚拟机;Rob Pike,Go 语言项目总负责人,贝尔实验室 Unix 团队成员,参与的项目包括 Plan 9,Inferno 操作系统和 Limbo 编程语言;Ken Thompson,贝尔实验室 Unix 团队成员,C 语言、Unix 和 Plan 9 的创始人之一,与 Rob Pike 共同开发了 UTF-8 字符集规范。2009 年 11 月 10 日,开发团队将 Go 语言项目以 BSD-style 授权(完全开源)正式公布了 Linux 和 Mac OS X 平台上的版本。Hector Chu 于同年 11 月 22 日公布了 Windows 版本。
作为一个开源项目,Go 语言借助开源社区的有生力量达到快速地发展,并吸引更多的开发者来使用并改善它。
- Go语言的特性
-
Go 语言将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行。因此,Go 语言是一门类型安全和内存安全的编程语言。虽然 Go 语言中仍有指针的存在,但并不允许进行指针运算。
-
Go 语言对网络通信、并发和并行编程有着极佳的支持,这能更好地利用大量的多核计算机。设计者通过 goroutine 这种轻量级线程的概念来实现这个目标,然后通过 channel 来实现各个 goroutine 之间的通信。这个特性是 Go 语言最强有力的部分,不仅支持了日益重要的多核与多处理器计算机,也弥补了现存编程语言在这方面所存在的不足。
-
Go 语言中另一个非常重要的特性就是它的构建速度(编译和链接到机器代码的速度),一般情况下构建一个程序的时间只需要数百毫秒到几秒。这不仅极大地提升了开发者的生产力,同时也使得软件开发过程中的代码测试环节更加紧凑,而不必浪费大量的时间在等待程序的构建上。
-
由于内存问题(通常称为内存泄漏)长期以来一直伴随着 C++ 的开发者们,Go 语言的设计者们认为内存管理不应该是开发人员所需要考虑的问题。因此尽管 Go 语言像其它静态语言一样执行本地代码,但它依旧运行在某种意义上的虚拟机,以此来实现高效快速的垃圾回收。
-
有许多活跃的开源项目都是基于Go语言进行开发,目前十分流行的Docker也是基于Go语言进行开发。
一、环境变量和常用命令
1.1 Go语言环境变量
Go语言开发环境依赖一些操作系统环境变量,常用的环境变量如下:
$GOROOT
表示 Go 在你的电脑上的安装位置,它的值一般都是 $HOME/go,当然,你也可以安装在别的地方。$GOARCH
表示目标机器的处理器架构,它的值可以是 386,amd64 或 arm。$GOOS
表示目标机器的操作系统,它的值可以是 darwin,freebsd,linux 或 windows。$GOBIN
表示编译器和链接器的安装位置,默认是 $GOROOT/bin,如果你使用的是 Go 1.0.3 及以后的版本,一般情况下你可以将它的值设置为空,Go 将会使用前面提到的默认值。$GOPATH
表示工作路径,允许包含多个目录。当有多个目录时,请注意分隔符,多个目录的时候Windows是分号,Linux系统是冒号,当有多个GOPATH时,默认会将go get命令的内容放在第一个目录下。$GOPATH 目录约定有三个子目录:- src 存放源代码(比如:.go .c .h .s等)
- pkg 编译后生成的文件(比如:.a)
- bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中,如果有多个gopath,那么使用${GOPATH//: //bin:}/bin添加所有的bin目录)很多Go命令都依赖于此变量,例如go get命令会将获取到的包放到GOPATH中。
1.2 Go语言的命令行工具
Go语言自带有一套完整的命令操作工具,你可以通过在shell中输入go来查看。
常用的命令。
go build
主要用于测试编译。在包的编译过程中,若有必要,会同时编译与之相关联的包。- 如果是普通包,当你执行go build之后,它不会产生任何文件。
- 如果你需要在$GOPATH/pkg下生成相应的文件,那就得执行go install了。
- 如果是main包,当你执行go build之后,它就会在当前目录下生成一个可执行文件。
- 如果你需要在$GOPATH/bin下生成相应的文件,需要执行go install。
- go build会忽略目录下以“_”或“.”开头的go文件。
go fmt
有过C/C++经验的读者会知道,一些人经常为代码采取K&R风格还是ANSI风格而争论不休。在go中,代码则有标准的风格。由于之前已经有的一些习惯或其它的原因我们常将代码写成ANSI风格或者其它更合适自己的格式,这将为人们在阅读别人的代码时添加不必要的负担,所以Go语言强制了代码格式(比如左大括号必须放在行尾),不按照此格式的代码将不能编译通过,为了减少浪费在排版上的时间,go工具集中提供了一个go fmt命令 它可以帮你格式化你写好的代码文件,使你写代码的时候不需要关心格式,你只需要在写完之后执行go fmt <文件名>.go
,你的代码就被修改成了标准格式。go install
这个命令在内部实际上分成了两步操作:第一步是生成结果文件(可执行文件或者.a包),第二步会把编译好的结果移到$GOPATH/pkg或者$GOPATH/bin。go test
执行这个命令,会自动读取源码目录下面名为*_test.go的文件,生成并运行测试用的可执行文件。默认的情况下,不需要任何的参数,它会自动把你源码包下面所有test文件测试完毕,当然你也可以带上参数,详情请参考go help testflag
。go run
编译并运行Go语言源代码,这对于我们快速运行测试代码非常方便。
Go语言还有其他一些命令参数,如go env
, godoc
,详细信息可以通过go help
进行查看。
二、GO语言的基础语法
2.1 标示符和关键字
Go语言的标示符(变量名,函数名)是一个非空的字母或数字串,其中第一个字符必须是字母,该字符也不能是关键字的名字。Go语言一共有25个关键字,如下:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
Go语言同样预定义了许多标示符,可以很方便的使用,如下:
- 类型:
bool byte complex64 complex128 error float32 float64
int int8 int16 int32 int64 rune string
uint uint8 uint16 uint32 uint64 uintptr
- 常量:
true false iota
- 零值:
nil
- 函数:
append cap close complex copy delete imag len
make new panic print println real recover
空标示符"_"
是一个占位符,它用于在赋值操作的时候将某个值赋值给空标示符号,从而达到丢弃该值的目的。空标示符不是一个新的变量,因此将它用于:=
操作符号的时候,必须同时为至少另一个值赋值。下面有几个例子:
count, err = fmt.Println(x) // 获取打印的字节数以及相应的error值
count, _ = fmt.Println(x) // 获取打印的字节数,并且丢弃error值
2.2 常量和变量
常量使用关键字const
声明,变量可以使用关键字var
声明,也可以通过使用快捷变量声明语法:=
。Go语言可以自动推断出所声明变量的类型。对于没有显式初始化的变量,Go语言总是将零值赋值给该变量。**在Go语言中,声明变量的时候类型名总是在变量名的后面。**下面有几个例子:
const limit = 512 // 常量,其类型兼容任何数字
const top uint16 = 1421 // 常量,类型:uint16
last := 1.5 // 变量,推断类型 flat64
var a int // 变量,值为0,类型 int
var debug = false // 变量,推断类型 bool
当需要设置多个常量的时候,不必重复使用const关键字,可以使用以下分组语法(var声明同样可以使用):
const (
Cyan = 0
Black = 1
White = 2
)
2.3 数值类型和布尔类型
Go语言提供了大量的内置数据类型,每一个数值类型都不同,意味着不能在不同类型之间进行二进制数值运算或者比较操作。无类型的数值常量可以兼容内置的任何类型的数值,在不同类型数值之间进行运算或者比较操作,需要进行类型转换。类型转换采用type(value)的方式,只要合法就能转换成功,即使会导致数据精度丢失。
- Go语言提供了11种整型,如下列表所示
类型 | 说明 |
---|---|
byte | 等同于uint8 |
int | 依赖于不同平台下的实现,可以是int32或者int64 |
int8 | [-128, 127] |
int16 | [-32768, 32767] |
int32 | [-2147483648, 2147483647] |
int64 | [-9223372036854775808, 9223372036854775807] |
rune | 等同于uint32 |
uint | 依赖于不同平台下的实现,可以是uint32或者uint64 |
uint8 | [0, 255] |
uint16 | [0, 65535] |
uint32 | [0, 4294967295] |
uint64 | [0, 18446744073709551615] |
uintptr | 一个可以恰好容纳指针值的无符号整型(对32位平台是uint32, 对64位平台是uint64) |
在Go语言中可以通过unsafe.Sizeof函数类型的字节长度。
- Go语言提供了两种浮点类型和两种复数类型, 具体如下:
类型 | 说明 |
---|---|
float32 | ±3.402 823 466 385 288 598 117 041 834 845 169 254 40x10E38 |
计算精度大概是小数点后7个十进制数 | |
float64 | ±1.797 693 134 862 315 708 145 274 237 317 043 567 981x10E38 |
计算精度大概是小数点后15个十进制数 | |
complex32 | 复数,实部和虚部都是float32 |
complex64 | 复数,实部和虚部都是float64 |
- Go语言提供了内置的布尔值
true
和false
。Go语言支持标准的逻辑和比较操作,这些操作的结果都是布尔值。值得注意的地方是可以通过!b
的方式反转变量b
的真假。
2.4、字符串
Go语言中的字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。UTF-8 是被广泛使用的编码格式,是文本文件的标准编码,其它包括 XML 和 JSON 在内,也都使用该编码。由于该编码对占用字节长度的不定性,Go 中的字符串也可能根据需要占用 1 至 4 个字节,这与其它语言如 C++、Java 或者 Python 不同。Go 这样做的好处是不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。
Go语言中字符串的可以使用双引号( " )或者反引号( ` )来创建。双引号用来创建可解析的字符串,所谓可解析的是指字符串中的一些符号可以被格式化为其他内容,如"\n"在输出时候会被格式化成换行符, 如果需要按照原始字符输出必须进行转义。而反引号创建的字符串原始是什么样,那输出还是什么,不需要进行任何转义。
Go语言中的部分转义字符如下表所示:
转义字符 | 含义 |
---|---|
\ | 表示反斜线 |
’ | 单引号 |
" | 双引号 |
\n | 换行符 |
\uhhhh | 4个16进制数字给定的Unicode字符 |
在Go语言中单个字符可以使用单引号( ’ )来创建。在Go语言中,一个单一的字符可以用一个单一的rune来表示,它等同于unint32。这也是容易理解的,因为Go语言的字符串是UTF-8编码,其底层使用4个字节表示,也就是32 bit。
在Go语言中,字符串支持切片操作,但是需要注意的是如果字符串都是由ASCII字符组成,那可以随便使用切片进行操作,但是如果字符串中包含其他非ASCII字符,直接使用切片获取想要的单个字符时需要十分小心,因为对字符串直接使用切片时是通过字节进行索引的,但是非ASCII字符在内存中可能不是由一个字节组成。如果想对字符串中字符依次访问,可以使用range操作符。另外获取字符串的长度可能有两种含义,一种是指获取字符串的字节长度,一种是指获取字符串的字符数量。字符串支持以下操作:
语法 | 描述 |
---|---|
s += t | 将字符串t追加到s末尾 |
s + t | 将字符串s和t级联 |
s[n] | 从字符串s中索引位置为n处的原始字节 |
s[n:m] | 从位置n到位置m-1处取得的字符(字节)串 |
s[n:] | 从位置n到位置len(s)-1处取得的字符(字节)串 |
s[:m] | 从位置0到位置m-1处取得的字符(字节)串 |
len(s) | 字符串s中的字节数 |
len([]rune(s)) | 字符串s中字符的个数,可以使用更快的方法utf8.RuneCountInString() |
[ ]rune(s) | 将字符串s转换为一个unicode值组成的串 |
string(chars) | chars类型是[]rune或者[]int32, 将之转换为字符串 |
[ ]byte(s) | 无副本的将字符串s转换为一个原始的字节的切片数组,不保证转换的字节是合法的UTF-8编码字节 |
格式化字符串
Go语言标准库中的fmt
包提供了打印函数将数据以字符串形式输出到控制台,文件,其他满足io.Writer
接口的值以及其他字符串。目前为止我们使用了fmt.Printf
和fmt.Println
,对于前者的使用,可以提供一些格式化指令,让Go语言对输出的字符串进行格式化。同样可以使用一些格式化修饰符,改变格式化指令的输出结果, 如左对齐等。常用的格式化指令如下:
格式化指令 | 含义 |
---|---|
%% | %字面量 |
%b | 一个二进制整数,将一个整数格式化为二进制的表达方式 |
%c | 一个Unicode的字符 |
%d | 十进制数值 |
%o | 八进制数值 |
%x | 小写的十六进制数值 |
%X | 大写的十六进制数值 |
%U | 一个Unicode表示法表示的整形码值,默认是4个数字字符 |
%s | 输出以原生的UTF-8字节表示的字符,如果console不支持UTF-8编码,则会输出乱码 |
%t | 以true或者false的方式输出布尔值 |
%v | 使用默认格式输出值,或者使用类型的String()方法输出的自定义值,如果该方法存在的话 |
%T | 输出值的类型 |
常用的格式化指令修饰符如下:
空白
如果输出的数字为负,则在其前面加上一个减号"-
"。如果输出的是整数,则在前面加一个空格
。使用%x
或者%X
格式化指令输出时,会在结果之间添加一个空格
。例如fmt.Printf("% X", "实")
输出E5 AE 9E
#
%#o
输出以0开始的八进制数据%#x
输出以0x开始的十六进制数据
+
让格式化指令在数值前面输出+号或者-号,为字符串输出ASCII字符(非ASCII字符会被转义),为结构体输出其字段名-
让格式化指令将值向左对齐(默认值为像右对齐)0
让格式指令以数字0而非空白进行填充
常用字符串包
- strings包提供了如查找字符串,分割字符串,判断前后缀,判断字符串包含,字符串替换,统计字符串出现的次数等常用操作,完整的方法列表可以参考官方包说明。
- strconv包提供了许多可以在字符串和其他类型的数据之间进行转换的函数。例如可以将数字转换为字符串,将数字样式的字符串转换为数值(将字符串"12345"转换int类型的整数)。
2.5、值、指针和引用类型
通常情况下Go语言中的变量持有相应的值。也就是说,我们可以将一个变量想象成它所持有的值来使用。其中有些例外,通道
、函数
、方法
、映射
、切片
是引用变量
,它们持有的都是引用,也即保存指针的变量。值在传递给函数或者方法的时候会被复制一次,对于布尔类型和数值类型来说这非常廉价,但是对于大型变量代价却非常大。而且复制传参的方式,修改值只是修改了副本,这能保证原始变量不被修改,但也一定程度上增加了修改原始值的麻烦。幸好在Go语言中有指针,使用指针时,我们每次传递给函数或者方法的只是变量的内存地址,这是非常廉价的。而且一个被指针指向的变量可以通过该指针来修改,这就很方便的在函数或者方法中通过指针修改原始变量。Go语言中的指针操作符也是使用&
和*
操作符,其中&
用于取地址,*
用于解引用,也就是获取指针指向的值。
2.6、数组和切片
1 数组
Go语言的数组是一个定长的序列,其中的元素类型相同。多维数组可以简单地使用自身为数组的元素来创建。数组的元素使用操作符号[ ]
来索引,索引从0
开始,到len(array)-1
结束。数组使用以下语法创建:
- [length]Type
- [N]Type{value1, value2, …, valueN}
- […]Type{value1, value2, …, valueN}
如果使用了...
(省略符)操作符,Go语言会为我们自动计算数组的长度。在任何情况下,一个数组的长度都是固定的并且不可修改。数组的长度可以使用len()
函数获得。由于数组的长度是固定的,因此数组的长度和容量都是一样的,因此对于数组而言cap()
和len()
函数返回值都是一样的。数组也可以使用和切片一样的语法进行切片,只是其结果为一个切片,而非数组。同样的,数组也可以使用range
进行索引访问。
2 切片
一般而言,Go语言的切片比数组更加灵活,强大而且方便。数组是按值传递的(即传递的副本),而切片是引用类型,传递切片的成本非常小,而且是不定长的。而且数组是定长的,而切片可以调整长度。创建切片的语法如下:
- make([ ]Type, length, capacity)
- make([ ]Type, length)
- [ ]Type{}
- [ ]Type{value1, value2, …, valueN}
内置函数make()
用于创建切片
、映射
和通道
。当用于创建一个切片时,它会创建一个隐藏的初始化为零值的数组,然后返回一个引用该隐藏数组的切片。该隐藏的数组与Go语言中的所有数组一样,都是固定长度,如果使用第一种语法创建,那么其长度为切片的容量capacity;如果是第二种语法,那么其长度记为切片的长度length。一个切片的容量即为隐藏数组的长度,而其长度则为不超过该容量的任意值。另外可以通过内置的函数append()
来增加切片的容量。
2.7、映射(map)
Go语言中的映射(map)是一种内置的数据结构,保存键=值
的无序集合,它的容量只受到机器内存的限制,类似于Python中的字典。在一个映射中所有的键
都是唯一的而且必须是支持==
和!=
操作符的类型,大部分Go语言的基本类型都可以作为映射的键,但是切片
、不能用于比较的数组
、结构体
(这些类型的成员或者字段不支持==
和!=
操作)或者基于这些的自定义类型不能作为键。但是任意类型都可以作为值。映射是引用类型,所以传递非常廉价。
Go语言中的映射可以用以下用法创建:
- make(map[KeyType]VauleType, initialCapacity)
- make(map[KeyType]ValueType)
- map[KeyType]ValueType{ }
- map[KeyType]ValueType{key1: value1, key2: value2, …, keyN: valueN}
内置的函数make()
可以用来创建切片、映射和channel(通道)。当用make()来创建一个映射时候,实际上是得到一个空映射,如果指定了容量(initialCapacity)就会预先申请足够的内存,并随着加入的项越来越多,映射会自动扩容。映射支持以下操作:
语法 | 含义 |
---|---|
m[k] = v | 用键k 来将值赋值v 给映射m。如果映射m中的k已存在,则将之前的值舍弃 |
delete(m, k) | 将键k 及其相关的值从映射m中删除,如果k不存在则不执行任何操作 |
v := m[k] | 从映射m中取得键k相对应的值并赋值给v。如果k不存在,则将映射类型的0值赋值v |
v, found := m[k] | 从映射m中取得键k相对应的值并赋值给v, 并将found的值赋值为true。如果k不存在,则found为false |
len(m) | 返回映射m中的项 |
k := range m | 遍历映射m中的键 |
k, v := range m | 同时遍历映射中的键和值 |
三、Go语言面向过程编程
先学习过程式编程,是因为在Go语言中面向对象编程也是建立在面向过程的基础上的。形式上讲,Go语言需要使用分号(;
) 来作为上下文语句的分隔结束符。实际上在代码中很少使用分号,那是因为编译器会自动在需要分号的地方加上分号。但是有两个地方必须使用分号,第一个是需要在一个行中放入一条或多条语句时,或者是在使用原始的 for 循环时。Go语言也支持多重赋值,如a, b = b, a。另外在之前的课程中我们提到过快速声明操作符:=
,它的作用是同时在一个语句中声明和赋值一个变量。当:=
操作符用于多个逗号分隔的变量时,如果该变量已经存在,则只是简单的修改它的值。但是当:=
操作符位于作用域的起始处时,Go语言会创建一个新的变量,不管该变量之前是否存在,如在if或者for语句中。下面有一个例子可以说明:
a, b, c := 2, 3, 5
for a := 7; a < 8; a++ {
fmt.Println(a)
}
以上代码中,先使用:=
声明并赋值了三个变量,Go会自动推导出变量的类型。然后再for语句处又一次使用:=
操作符声明了变量a。需要注意的地方是,for语句代表了一个新的作用域,所以:=
在这里新声明创建了一个变量a,这个变量和之前的变量a是完全不同的两个变量(内存地址不一样),所以是一个影子变量,因为覆盖了外部的同名变量。这是需要注意的一个地方。
3.1 类型转换
Go语言提供了一种在不同但相互兼容的类型之间相互转换的方式,这种转换非常有用并且是安全的。但是需要注意的是在数值之间进行转换可能造成其他问题,如精度丢失或者错误的结果。以下是类型转换的语法:
resultOfType := Type(expression)
几个例子:
x := int16(2345) // 声明一个类型为int16的整数,其值为2345
y := int32(x) // 将int16类型的整数转换为int32类型
a := uint16(6500) // 声明一个类型为uint16类型的整数
b := int16(a) // 转换为int16类型,虽然能转换成功,但是由于6500超过in16类型的范围,会导致结果错误,b的值为 -536
另外在Go语言中可以通过type
关键字声明类型,如type StringsSlice []string
将[]string
(string类型的切片)声明为StringSlice
类型。
3.2 类型断言
说到类型断言就需要先了解下Go语言中的接口。在Go语言中接口是一个自定义类型。它声明了一个或者多个方法。任何实现了这些方法的对象(类型)都满足这个接口。
接口是完全抽象的,不能实例化。interface{}
类型表示一个空接口,任何类型都满足空接口。也就是说 interface{}
类型的值可以用于表示任意 Go 语言类型的值。
这里的空接口有点类似于 Python 语言中的 object 实例。** 既然 interface{} 可以用于表示任意类型,那有的时候我们需要将 interface{} 类型转换为我们需要的类型,这个操作称为类型断言。**一般情况下只有我们希望表达式是某种特定类型的值时才使用类型断言。
Go语言中可以使用以下语法:
resultOfType, boolean := expression.(Type) // 安全的类型断言
resultOfType := expression.(Type) // 非安全的类型断言,失败时程序会产生异常
3.3 分支和for语句
Go语言提供了3种分支,即if、switch、select, 其中select用于监听channel(通道)。
if分支
语法:
if optionalStatement1; booleanExpression1 {
block1
} else if optionalStatement2; booleanExpression2 {
block2
} else {
block3
}
其中optionalStatement
是可选的表达式,真正决定分支走向的是booleanExpression1
的值。
switch分支
Go语言中switch
分支既可用于常用的分支就象C语言中的switch一样,也可以用于类型开关,所谓类型开关就是用于判断变量属于什么类型。但是需要注意的是Go语言的switch语句不会自动贯穿,相反,如果想要贯穿需要添加fallthrough
语句。表达式开关switch的语法如下:
switch optionalStatement; optionalExpression {
case expression1: block1
...
case expressionN: blockN
default: blockD
}
例子:
switch { // 没有表达式,默认为True值,匹配分支中值为True的分支
case value < minimum:
return minimum
case value > maximum:
return maximum
default:
return value
}
在上面的例子中,switch后面没有默认的表达式,这个时候Go语言默认其值为True。 在前面提到过类型断言,如果我们知道变量的类型就可以使用类型断言,但是当我们知道类型可能是许多类型中的一种时候,我们就可以使用类型开关。其语法如下:
switch optionalStatement; typeSwitchGuard {
case type1: block1
...
case typeN: blockN
default: blockD
}
for循环
它可以遍历数组,切片,映射等类型,也可以用于无限循环。以下是其语法:
for { // 无限循环
block
}
for booleanExpression { // while循环,在Go语言中没有while关键字
}
for index, char := range aString { // 迭代字符串
}
for item := range aChannel { // 迭代通道
}
3.4 函数
Go语言可以很方便的自定义函数,其中有特殊的函数main
函数。main
函数必须出现在main包
里,且只能出现一次。当Go程序运行时候会自动调用main
函数开始整个程序的执行。main
函数不可接收任何参数,也不返回任何结果。 Go语言中函数的创建使用以下语法:
func functionName(optionalParameters) optionalReturnType {
block // func 函数名(参数列表) 单个返回值类型
}
func functionName(optionalParameters) (optionalReturnValues) {
block // func 函数名(参数列表) (返回值1 类型,返回值2 类型)
}
函数可以有任意多个参数,也可以有任意多个返回值,返回值可以是命名的。具体的看以下例子:
func func1(first int, rest ...int) int {
return first // func1 函数可以接收多个int类型的参数,并且返回一个int类型的值
}
func func2(first int, second string) (int, string) {
return first, second // func2 接收两个参数,并且返回一个int和string类型的值
}
func func3(first int, second string) (a, b int) {
a, b = 1, 2 // func3 接收两个参数,并返回两个int类型的值,因为返回值是命名的,所以这里可以缩写
return
}
3.5 通道(channel)和并发(goroutine)
Go语言有强大的并发功能,这些功能都是建立在通道和并发语句上的。所谓goroutine
是程序中与其他goroutine
完全独立而并发执行的函数或者方法调用。每一个Go程序都至少有一个goroutine
,其中main()
函数所在的goroutine
是主goroutine
。goroutine
很像轻量级的线程,它们可以被大批量的创建。那goroutine
之间怎么进行通信呢?Go语言中推荐的做法是使用channel
(通道)。channel
是一个双向的或者单向的通信管道,可以用于两个或者多个goroutine
之间进行通信(即接收和发送)数据。
.1 语法
- goroutine使用以下的go语句进行创建:
go function(arguments)
go func(parameters) { block } (arguments)
第二种方式中,我们是创建了一个临时的匿名函数,并马上在 goroutine 中执行。 当调用 go
关键字执行函数时,函数会在另一个 goroutine 上马上执行,并且当前的 goroutine 的执行会从下一条语句马上恢复。因此执行一个go
语句之后,当前程序中至少有两个 goroutine 在运行。
在大多数情况下,goroutine之间需要相互协作,最好的方式是通过channel来交换数据。使用下面语法创建channel(通道):
make(chan Type)
make(chan Type, capacity)
语法中的Type
指明了通道能发送的数据类型。其中第一种语法中创建了一个同步的通道,一次只能发送一项数据,它会阻塞直到发送者准备好发送和接收者准备好接收。如果给定了capacity也就是缓冲区容量,在缓冲区容量未满之前通道都是异步无阻塞的。通道支持的操作如下:
语法 | 含义 |
---|---|
channel <- value | 发送value到通道中,有可能阻塞 |
<-channel | 从通道中接收数据 |
x := <-channel | 从通道接收数据并赋值给x |
x, ok := <-channel | 功能同上,同时检查通道是否已关闭或者是否为空 |
.2 select语句
select语句,用于监听通道。其语法如下:
select {
case sendOrReceviae1: block1
...
case sendOrReceiveN: blockN
default: blockD
}
Go语言会从头至尾的判断每一个case
中的发送和接收语句。如果其中任何一条语句可以执行(即没有被阻塞),那就从那些可执行的语句中任意选择一条来使用。如果所有的通道都被阻塞,那可能有两种情况。第一种,如果有default语句,那就会执行default 语句,同时程序的执行会从select语句恢复。第二种,如果没有default语句,则select语句会一直阻塞,直到有一个通道可用。
3.6、defer、panic和recover
1. defer
开发程序时,有的时候忘记关闭打开的文件导致程序执行失败,在python中可以很方便的使用with语句对这些资源进行自动管理。在Go中我们可以使用defer语句完成这项任务。defer
语句用于延迟执行一个函数或者方法或者是当前创建的匿名函数,它会在外部函数或者方法返回之前但是其返回值计算之后执行。这样就可能在一个延迟执行的函数中修改函数的命名返回值。如果一个函数中有多个defer语句,它们会以后进先出
的顺序执行。defer最常用的地方就是保证一个使用完成后的文件正常关闭。
如下例子:
var file *os.File
var err error
if file, err = os.Open(filename); err != ni {
do_something(file)
return
}
defer file.Close()
2. panic和recover
panic
类似于其他程序中的异常,而 recover
则用于恢复异常。当panic()函数被调用时,外围函数或者方法的执行会立即终止。然后任何延迟执行的函数都会被调用。这个过程一直在调用栈中层层发生,最后到达main函数,这个时候整个程序会终止,最终将最初的调用栈信息输出到stderr
。但是当延迟执行函数中包含recover
语句时,recover会捕捉到panic引发的异常,并停止panic的传播,这个时候我们能够以任何我们想用的方式处理panic。
Go语言将错误和异常两者区分对待。错误是指有可能出错的东西,程序中已经包含处理这些错误的优雅逻辑。而异常则是指不可能发生的事情。例如,一个永远为true的条件在实际环境中却是false。Go语言推荐使用错误,而不使用异常。通常情况下,我们可以在recover中阻止panic的传播,并将recover()的返回值转换成错误。
四、Go语言面向对象编程
在讲解Go语言面向对象内容之前,需要说明下Go语言的代码是以包
结构来组织的,且如果标示符(变量名,函数名,自定义类型等)以大写字母开头那么这些标示符是可以导出的,可以在任何导入了定义该标示符的包的包中直接使用。Go语言中的面向对象和C++,Java中的面向对象不同,因为Go语言不支持继承,Go语言只支持聚合。
4.1 自定义类型
在Go语言中我们可以自定义类型,其语法如下:
type typeName typeSpecification
其中,typeName
可以是一个包或者函数内唯一合法的Go标示符。typeSpecification
可以是任何内置的类型,一个接口或者是一个结构体。所谓结构体,它的字段是由其他类型或者接口组成。例如我们通过结构体定义了一下类型:
type ColorPoint struct {
color.Color // 匿名字段(嵌入)
x, y int // 具名字段(聚合)
}
以上代码我们通过结构体自定义了类型ColorPoint
,结构体中color.Color
字段是Color包的类型color,这个字段没有名字,所以被称为匿名的,也是嵌入字段
。字段x和y是有变量名的,所以被称为具名字段
。假如我们创建了类型ColorPoint的一个值point(通过语法:point := ColorPoint{} 创建),那么这些字段可以通过point.Color、point.x、point.y访问。其他面向对象语言中的"类(class)"、“对象(object)”、"实例(instance)"在Go语言中我们完全避开使用。相反的我们使用"类型(type)“和其对应的"值”,其中自定义类型的值可以包含方法。
4.2 方法
方法是作用在自定义类型上的一类特殊函数,通常自定义类型的值会被传递给该函数,该值可能是以指针或者复制值的形式传递。定义方法和定义函数几乎相同,只是需要在func关键字和方法名之间必须写上接受者。例如我们给类型Count定义了以下方法:
type Count int
func (count *Count) Increment() { *count++ } // 接受者是一个`Count`类型的指针
func (count *Count) Decrement() { *count-- }
func (count Count) IsZero() bool { return count == 0 }
以上代码中,我们在内置类型int的基础上定义了自定义类型Count,然后给该类型添加了Increment()、Decrement()和IsZero()方法,其中前两者的接受者为Count类型的指针,后一个方法接收Count类型的值。
类型的方法集是指可以被该类型的值调用的所有方法的集合。
一个指向自定义类型的值的指针,它的方法集由该类型定义的所有方法组成,无论这些方法接受的是一个值还是一个指针。如果在指针上调用一个接受值的方法,Go语言会聪明地将该指针解引用。
一个自定义类型值的方法集合则由该类型定义的接收者为值类型的方法组成,但是不包括那些接收者类型为指针的方法。
其实这些限制Go语言帮我们解决的非常好,结果就是我们可以在值类型上调用接收者为指针的方法。假如我们只有一个值,仍然可以调用一个接收者为指针类型的方法,这是因为Go语言会自动获取值的地址传递给该方法,前提是该值是可寻址的。
在以上定义的类型Count中,*Count方法集是Increment(), Decrement()和IsZero(),Count的值的方法集是IsZero()。但是因为Count类型的是可寻址的,所以我们可以使用Count的值调用全部的方法。 另外如果结构体的字段也有方法,我们也可以直接通过结构体访问字段中的方法。
4.3 接口
Go语言的面向对象很灵活,很大一部分原因是由于接口的存在。接口是一个自定义类型,它声明了一个或者多个方法签名,任何实现了这些方法的类型都实现这个接口。infterface{}
类型是声明了空方法集的接口类型。任何一个值都满足interface{}类型,也就是说如果一个函数或者方法接收interface{}类型的参数,那么任意类型的参数都可以传递给该函数。接口是完全抽象的,不能实例化。接口能存储任何实现了该接口的类型。
接口类型声明的变量里能存储任何实现了该接口的类型的值。有的时候我们需要知道这个变量里的值的类型,那么需要怎么做呢?其实在之前的课程中我们就已经学习过了,可以使用类型断言,或者是switch类型判断分支。
在接口里也可以再嵌入接口。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里的方法。如下例子中, Interface2包含了Interface1的所有方法。
type Interface1 interface {
Send()
Receive()
}
type Interface2 interface {
Interface1
Close()
}
五、 包
Go语言组织代码的方式是包
,包是各种类型和函数的集合。在包中,如果标示符(类型名称,函数名称,方法名称)的首字母是大写
,那这些标示符是可以被导出
的,也就是说可以在包以外直接使用。前面我们也提到了$GOPATH
环境变量(指向一个或多个目录),以及其子目录src
目录,当我们使用import
关键字导入包的时候,Go语言会在$GOPATH
,$GOROOT
目录中搜索包。
我们创建的自定义的包最好放在$GOPATH
的src
目录下,如果这个包只属于某个应用程序,可以直接放在应用程序源代码的子目录下,但如果我们希望这个包可以被其他的应用程序共享,那就应该放在$GOPATH
的src
目录下,每个包单独放在一个目录里,如果两个不同的包放在同一目录下,会出现名字冲突的编译错误。作为惯例,包的源代码应该放在一个同名的文件夹下面。同一个包可以有任意多的源文件,文件名的名字也没有任何规定。