第一部分:学习Go语言
第一章:起源于发展
起源与发展
Go 语言起源 2007 年,并于 2009 年正式对外发布。
时间轴:
- 2007 年 9 月 21 日:雏形设计
- 2009 年 11 月 10日:首次公开发布
- 2010 年 1 月 8 日:当选 2009 年年度语言
- 2010 年 5 月:谷歌投入使用
- 2011 年 5 月 5 日:Google App Engine 支持 Go 语言
主要特性
发展目标:将静态语言的安全性和高效性与动态语言的易开发性进行有机结合。是对于网络通信、并发和并行编程的极佳支持,从而更好地利用大量的分布式和多核的计算机。
Go 语言是一门类型安全和内存安全的编程语言。虽然 Go 语言中仍有指针的存在,但并不允许进行指针运算。
重要的特性:
- 构建速度(编译和链接到机器代码的速度)快。
- 使用包模式的依赖管理更加的清晰。
- 执行速度快。
- 没有类和继承的概念,通过接口(interface)的概念来实现多态性。
- 使用静态类型,所以它是类型安全的一门语言。
- 强类型语言,隐式的类型转换是不被允许。
- 动态语言的特性(通过关键字
var
)。 - 支持交叉编译。
LALR 是 Go 语言的语法标准。
Go语言的用途:
- 应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。
- 实现所谓的复杂事件处理(CEP)。
- Go 语言不适合用来开发对实时性要求很高的软件。
通过 recover
和 panic
来替代异常机制
第二章:安装与运行环境
架构
2个版本的编译器:Go 原生编译器 gc 和非原生编译器 gccgo,这两款编译器都是在类 Unix 系统下工作 。
Go从1.5版本开始已经实现自举。
Go 语言源文件的扩展名很显然就是 .go
。
创建目录时,文件夹名称永远不应该包含空格,而应该使用下划线 “_” 或者其它一般符号代替。
环境变量
- $GOROOT 表示 Go 在你的电脑上的安装位置
- $GOARCH 表示目标机器的处理器架构
- $GOBIN 表示编译器和链接器的安装位置
- $GOPATH三个规定的目录:
src
、pkg
和bin
,这三个目录分别用于存放源码文件、包文件和可执行文件。
Go 编译器支持交叉编译,可以使用 $GOHOSTOS
和 $GOHOSTARCH
设置本地机器的操作系统名称和编译体系结构。
安装目录
/bin
:包含可执行文件/doc
:包含示例程序,代码工具,本地文档等/lib
:包含文档模版/src
:包含源代码构建脚本和标准库的包的完整源代码
Go运行时(runtime)
代码仍旧运行在 Go 的 runtime当中。似 Java 和 .NET 语言所用到的虚拟机,它负责管理包括内存分配、垃圾回收)、栈处理、goroutine、channel、切片(slice)、map 和反射(reflection)等等。
垃圾回收器Go 拥有简单却高效的标记-清除回收器。
第三章:编辑与调试
编辑器与调试器
编辑器:**Sublime Text、LiteIDE、GoClipse**
调试器:可用的调试器是 gdb。
基本调试:
- 在合适的位置使用打印语句输出相关变量的值。
- 在
fmt.Printf
中使用下面的说明符%+v
打印包括字段在内的实例的完整信息%#v
打印包括字段和限定类型名称在内的实例的完整信息。%T
打印某个类型的完整说明
构建
格式化工具 gofmt
并保存格式化后的源文件。
构建应用程序:
go build
编译自身包和依赖包。go install
编译并安装自身包和依赖包。
格式化代码
go fmt
(gofmt
)。这个工具可以将你的源代码格式化成符合官方统一标准的风格,属于语法风格层面上的小型重构。
gofmt –w program.go
会格式化该源文件的代码然后将格式化后的代码覆盖原始内容。
gofmt -w *.go
会格式化并重写所有 Go 源文件。
gofmt map1
会格式化并重写 map1 目录及其子目录下的所有 Go 源文件。。
gofmt
也可以通过在参数 -r
后面加入用双引号括起来的替换规则实现代码的简单重构,规则的格式:<原始内容> -> <替换内容>
。
1 | gofmt -r '(a) -> a' -w *.go |
生成代码文档
go doc
工具会从 Go 程序和包文件中提取顶级声明的首行注释以及每个对象的相关注释,并生成相关文档。
一般用法
go doc package
获取包的文档注释go doc package/subpackage
获取子包的文档注释go doc package function
获取某个函数在某个包中的文档注释
第四章:基本结构和基本数据类型
文件名、关键字与标识符
文件名均由小写字母组成,如 scanner.go
。如果文件名由多个部分组成,则使用下划线 _
对它们进行分隔,scanner_test.go
。
有效的标识符必须以字母(可以使用任何 UTF-8 编码的字符或 _
)开头。
_
本身就是一个特殊的标识符,被称为空白标识符。
程序一般由关键字、常量、变量、运算符、类型和函数组成。
程序的基本结构和要素
包、导入与可见性
包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。
每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go
为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。
package main
表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main
的包。
所有的包名都应该使用小写字母。
属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。
每一段代码只会被编译一次
导入包的方式
1 | import "fmt" |
可见性规则
当标识符以一个大写字母开头,就可以被外部包的代码所使用,这被称为导出;标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用。
可以像面向对象语言那样使用点标记来调用:pack1.Thing
函数
1 | func functionname() |
main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
函数简短
1 | func Sum(a,b int) int {return a+b} |
只有当某个函数需要被外部包调用的时候才使用大写字母开头。
注释
注释不会被编译,但可以通过 godoc 来使用。
以 //
开头的单行注释。多行注释也叫块注释,均已以 /*
开头,并以 */
结尾
每一个包应该有相关注释,在 package 语句之前的块注释将被默认认为是这个包的文档说明。
几乎所有全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释。如果这种注释(称为文档注释)出现在函数前面,例如函数 Abcd,则要以 "Abcd..."
作为开头。
1 | // enterOrbit causes Superman to fly into low Earth orbit, a position |
类型
类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、slice、map、channel;只描述类型的行为的,如:interface。
结构化的类型没有真正的值,它使用 nil 作为默认值。Go 语言中不存在类型继承。
函数也可以是一个确定的类型,就是以函数作为返回类型。
一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 ()
将它们括起来,如:
1 | func Functionname (a int,b int) (c int,d int) |
type 关键字可以定义你自己的类型,你可能想要定义一个结构体,但是也可以定义一个已经存在的类型的别名,如:
1 | type IZ int |
并不是真正意义上的别名,使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。
多个类型定义:
1 | type ( |
程序的一般结构
在完成包的 import 之后,开始对常量、变量和类型的定义或声明。
如果存在 init 函数的话,则对该函数进行定义。
如果当前包是 main 包,则定义 main 函数。
然后定义其余的函数,首先是类型的方法,接着是按照 main 函数中先后调用的顺序来定义相关函数。
1 | package main |
类型转换
Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明。
类型 B 的值 = 类型 B(类型 A 的值)
1 | a := 5.0 |
常量
常量使用关键字 const
定义,用于存储不会改变的数据。
存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
1 | const Pi = 3.14 |
常量的值必须是能够在编译时就能够确定的。
因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()。
1 | const Ln2 = 0.693147180559945309417232121458\ |
反斜杠 \
可以在常量表达式中作为多行的连接符使用。
常量并行赋值
1 | const beef,two,c = "eat",2,"vag" |
iota
可以被用作枚举值:
1 | const ( |
简单地讲,每遇到一次 const 关键字,iota 就重置为 0。.
变量
声明变量的一般形式是使用 var
关键字:var identifier type
。
1 | var a int |
变量的命名规则遵循骆驼命名法。
全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写。
变量可以编译期间就被赋值,赋值给变量使用运算符等号 =
,可以在运行时对变量进行赋值操作。
1 | var a int |
声明与赋值(初始化)语句也可以组合起来。
1 | var a int = 15 |
自动类型推断
1 | var a = 15 |
在函数体内声明局部变量时,应使用简短声明语法 :=
,例如:
1 | a := 1 |
实例:
1 | package main |
值类型和引用类型
int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值。
数组和结构体这些复合类型是值类型。
指针、切片、映射和通道是引用类型。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。
函数 fmt.Sprintf
与 Printf
的作用是完全相同的,不过前者将格式化后的字符串以返回值的形式返回给调用者。
在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20
就是不被允许的。
1 | a, b, c := 5, 7, "abc" |
交换两个变量的值,则可以简单地使用 a, b = b, a
。
init函数
变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。
每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。
1 | package trans |
1 | package main |
init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine。
1 | func init() { |
基本类型与运算符
bool类型
只有两个类型相同的值才可以进行比较,如果值的类型是接口,它们也必须都实现了相同的接口。
布尔型的常量和变量也可以通过和逻辑运算符(非 !
、和 &&
、或 ||
)结合来产生另外一个布尔值。
!
非运算符用于取得和布尔值相反的结果。
&&
两边的值都为 true 的时候,结果才是 true。
||
两边的值都为 false 的时候,结果才是 false。
在格式化输出时,你可以使用 %t
来表示你要输出的值为布尔型。
数字类型
Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。
Go 也有基于架构的类型,例如:int、uint 和 uintptr。
这些类型的长度都是根据运行程序所在的操作系统类型所决定的:
int
和uint
在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)。uintptr
的长度被设定为足够存放一个指针即可。
Go 语言中没有 float 类型。(Go语言中只有 float32 和 float64)没有double类型。
int 型是计算最快的一种类型。
float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。
尽可能地使用 float64,因为 math
包中所有有关数学运算的函数都会要求接收这个类型。
前缀 0 来表示 8 进制数(如:077),增加前缀 0x 来表示 16 进制数(如:0xFF),以及使用 e 来表示 10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)。
可以使用 a := uint64(0)
来同时完成类型转换和赋值操作,这样 a 的类型就是 uint64。
Go 中不允许不同类型之间的混合使用,但是对于常量的类型限制非常少,因此允许常量之间的混合使用:
1 | package main |
格式化说明符
%d
用于格式化整数(%x
和 %X
用于格式化 16 进制表示的数字),%g
用于格式化浮点型(%f
输出浮点数,%e
输出科学计数表示法),%0nd
用于规定输出长度为n的整数,其中开头的数字 0 是必须的。
%n.mg
用于表示数字 n 并精确到小数点后 m 位,除了使用 g 之外,还可以使用 e 或者 f,例如:使用格式化字符串 %5.2e
来输出 3.4 的结果为 3.40e+00
。
/
对于整数运算而言,结果依旧为整数,例如:9 / 4 -> 2
。
取余运算符只能作用于整数:9 % 4 -> 1
。
对于整数和浮点数,你可以使用一元运算符 ++
(递增)和 --
(递减),但只能用于后缀。
++
和 --
的只能作为语句,而非表达式,因此 n = i++
这种写法是无效的。
随机数
rand
包实现了伪随机数的生成。
1 | package main |
函数 rand.Intn
返回介于 [0, n) 之间的伪随机数。
类型别名
在 type TZ int
中,TZ 就是 int 类型的新名称(用于表示程序中的时区),然后就可以使用 TZ 来操作 int 类型的数据。
新类型不会拥有原类型所附带的方法。
字符类型
byte
类型是 uint8
的别名。
Go 同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。
rune
也是 Go 当中的一个类型,并且是 int32
的别名。
1 | var ch byte = 65 或 var ch byte = '\x41' |
var ch byte = 'A'
;字符使用单引号括起来。
Unicode 至少占用 2 个字节,所以我们使用 int16
或者 int
类型来表示。如果需要使用到 4 字节,则会加上 \U
前缀;前缀 \u
则总是紧跟着长度为 4 的 16 进制数,前缀 \U
紧跟着长度为 8 的 16 进制数。
1 | var ch int = '\u0041' |
- 判断是否为字母:
unicode.IsLetter(ch)
- 判断是否为数字:
unicode.IsDigit(ch)
- 判断是否为空白符号:
unicode.IsSpace(ch)
包 utf8
拥有更多与 rune 类型相关的函数。
字符串
字符串是一种值类型,且值不可变,字符串是字节的定长数组。
Go 中的字符串是根据长度限定,而非特殊字符\0
。
函数 len()
来获取字符串所占的字节长度。
在循环中使用加号 +
拼接字符串并不是最高效的做法,更好的办法是使用函数 strings.Join()。
使用字节缓冲(bytes.Buffer
)拼接更加给力。
strings和strconv包
Go 中使用 strings
包来完成对字符串的主要操作。
1 | strings.HasPrefix(s, prefix string) bool |
与字符串相关的类型转换都是通过 strconv
包实现的。
1 | strconv.Itoa(i int) string |
第五章:控制结构
if-else结构
1 | if condition { |
当 if 结构内有 break、continue、goto 或者 return 语句时,Go 代码的常见写法是省略 else 部分
1 | if condition { |
1 | if err != nil { |
swith结构
它可以接受任意形式的表达式:
1 | switch var1 { |
变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。
一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块。
继续执行后续分支的代码,可以使用 fallthrough
关键字
1 | switch i { |
可选的 default
分支可以出现在任何顺序,但最好将它放在最后。它的作用类似与 if-else
语句中的 else
,表示不符合任何已给出条件时,执行相关语句。
switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true),然后在每个 case 分支中进行测试不同的条件。
1 | switch { |
for结构
基于计数器的迭代
1 | package main |
基于条件判断的迭代
1 | package main |
for-range结构
for ix, val := range coll { }
。
val
始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值
1 | for pos, char := range str { |
标签和goto
for、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:
)
1 | package main |
第六章:函数
简介
函数是基本的代码块。函数编写的顺序是无关紧要的,最好把 main()
函数写在文件的前面。
简单的 return
语句也可以用来结束 for 死循环,或者结束一个协程(goroutine)。
Go里面的函数:
- 普通函数
- 匿名函数
- 方法
除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。
函数参数、返回值以及它们的类型被统称为函数签名。
函数是一等值(first-class value):它们可以赋值给变量,就像 add := binOp
一样。
函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数
函数参数与返回值
函数定义时,它的形参一般是有名字的,不过我们也可以定义没有形参名的函数,只有相应的形参类型,就像这样:func f(int, int, float64)
。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
命名返回值
命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。
1 | func getX2AndX3_2(input int) (x2 int, x3 int) { |
改变外部变量
传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return
返回。
1 | package main |
变长参数
函数的最后一个参数是采用 ...type
的形式,那么这个函数就可以处理一个变长的参数
1 | package main |
变长参数的类型不相同
使用结构体
使用空接口
使用默认的空接口
interface{}
,这样就可以接受任何类型的参数
new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。
new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针
make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作。
defer和追踪
关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return
语句之后)一刻才执行某个语句或函数。
1 | package main |
当有多个 defer 行为被注册时,它们会以逆序执行
1 | package mian |
函数作为参数
1 | package main |
闭包
匿名函数的使用
1 | package main |
函数作为返回值
1 | package main |
1 | package main |
闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。
计算函数的执行时间
1 | package main |
第七章:数组与切片
数组
1 | package main |
数组是 可变的。
Go 语言中的数组是一种 值类型,所以可以通过 new()
来创建:
1 | a := new([10]int) |
b的类型是*[10]int
, c的类型是[10]int
。
初始化
1 | package main |
切片
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。切片是一个 长度可变的数组。
切片是可索引的,并且可以由 len()
函数获取长度。计算容量的函数 cap()
可以测量切片最长可以达到多少。
1 | package main |
make创建数组
1 | package main |
1.slice、map以及channel都是golang内建的一种引用类型,三者在内存中存在多个组成部分, 需要对内存组成部分初始化后才能使用,而make就是对三者进行初始化的一种操作方式
2. new 获取的是存储指定变量内存地址的一个变量,对于变量内部结构并不会执行相应的初始化操作, 所以slice、map、channel需要make进行初始化并获取对应的内存地址,而非new简单的获取内存地址