Go语言学习笔记

Go语言学习笔记

参考(Ctrl c+v) https://www.kancloud.cn/kancloud/the-way-to-go/

当个学习笔记,只记录重点。

初始

平台与架构

Go 语言开发团队开发了适用于以下操作系统的编译器:

  • Linux
  • FreeBSD
  • Mac OS X(也称为 Darwin)

目前有2个版本的编译器:Go 原生编译器 gc 和非原生编译器 gccgo,这两款编译器都是在类 Unix 系统下工作 。其中,gc 版本的编译器已经被移植到 Windows 平台上,并集成在主要发行版中,你也可以通过安装 MinGW 从而在 Windows 平台下使用 gcc 编译器。这两个编译器都是以单通道的形式工作。

你可以获取以下平台上的 Go 1.4 源码和二进制文件:

  • Linux 2.6+:amd64、386 和 arm 架构
  • Mac OS X(Snow Leopard + Lion):amd64 和 386 架构
  • Windows 2000+:amd64 和 386 架构

Go 环境变量

  • $GOROOT 表示 Go 在你的电脑上的安装位置,它的值一般都是 $HOME/go,当然,你也可以安装在别的地方。
  • $GOARCH 表示目标机器的处理器架构,它的值可以是 386、amd64 或 arm。
  • $GOOS 表示目标机器的操作系统,它的值可以是 darwin、freebsd、linux 或 windows。
  • $GOBIN 表示编译器和链接器的安装位置,默认是 $GOROOT/bin,如果你使用的是 Go 1.0.3 及以后的版本,一般情况下你可以将它的值设置为空,Go 将会使用前面提到的默认值。

安装目录清单

  • /bin:包含可执行文件,如:编译器,Go 工具
  • /doc:包含示例程序,代码工具,本地文档等
  • /lib:包含文档模版
  • /misc:包含与支持 Go 编辑器有关的配置文件以及 cgo 的示例
  • /os_arch:包含标准库的包的对象文件(.a
  • /src:包含源代码构建脚本和标准库的包的完整源代码(Go 是一门开源语言)
  • /src/cmd:包含 Go 和 C 的编译器和命令行脚本

Go调试器

  1. 在合适的位置使用打印语句输出相关变量的值(print/printlnfmt.Print/fmt.Println/fmt.Printf)。
  2. fmt.Printf 中使用下面的说明符来打印有关变量的相关信息:
    • %+v 打印包括字段在内的实例的完整信息
    • %#v 打印包括字段和限定类型名称在内的实例的完整信息
    • %T 打印某个类型的完整说明
  3. 使用 panic 语句(第 13.2 节)来获取栈跟踪信息(直到 panic 时所有被调用函数的列表)。
  4. 使用关键字 defer 来跟踪代码执行过程(第 6.4 节)。

构建并运行 Go 程序

在大多数 IDE 中,每次构建程序之前都会自动调用源码格式化工具 gofmt 并保存格式化后的源文件。如果构建成功则不会输出任何信息,而当发生编译时错误时,则会指明源码中具体第几行出现了什么错误,如:a declared and not used。一般情况下,你可以双击 IDE 中的错误信息直接跳转到发生错误的那一行。

如果程序执行一切顺利并成功退出后,将会在控制台输出 Program exited with code 0

从 Go 1 版本开始,使用 Go 自带的更加方便的工具来构建应用程序:

  • go build 编译并安装自身包和依赖包
  • go install 安装自身包和依赖包

语言的核心结构与技术

基本结构和基本数据类型

hello world

1
2
3
4
5
6
7
8
9
package main

import (
"fmt"
)

func main() {
fmt.Println("hello, world")
}

包的概念

包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。

package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。

所有的包名都应该使用小写字母

注释

// 单行注释
/* xxxx */ 多行注释

函数

你可以在括号 () 中写入 0 个或多个函数的参数(使用逗号 , 分隔),每个参数的名称后面必须紧跟着该参数的类型。

1
func Sum(a, b int) int { return a + b }

类型

基本类型:intfloatboolstring

结构化的(复合的):structarrayslicemapchannel

结构化的类型没有真正的值,它使用nil作为默认值

类型转换

类型 B 的值 = 类型 B(类型 A 的值)

1
valueOfTypeB = typeB(valueOfTypeA)

常量

1
2
3
4
5
6
const beef, two, c = “meat”, 2, “veg”
const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6
const (
Monday, Tuesday, Wednesday = 1, 2, 3
Thursday, Friday, Saturday = 4, 5, 6
)

变量

声明变量时将变量的 类型 放在变量的 名称之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var identifier type

多行根据变量的值来自动推断其类型
var (
a = 15
b = false
str = "Go says hello to the world!"
numShips = 50
city string
)

并行\同时赋值
a, b, c = 5, 7, "abc"

值交换
a, b = b, a

变量的 命名规则 遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShipsstartDate

一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。

如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包、外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。

函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。

打印

函数 fmt.Printfmt.Println 会自动使用格式化标识符 %v 对字符串进行格式化,两者都会在每个参数之间自动增加空格,而后者还会在字符串的最后加上一个换行符。例如:

1
2
3
fmt.Print("Hello:", 23)
将输出:
Hello: 23

init 函数

变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。

不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。

一个源文件都可以包含且只包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。

init.go

1
2
3
4
5
6
7
8
9
package trans

import "math"

var Pi float64

func init() {
Pi = 4 * math.Atan(1) // init() function computes Pi
}

user_init.go 中导入了包 trans(在相同的路径中)并且使用到了变量 Pi

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"./trans"
)

var twoPi = 2 * trans.Pi

func main() {
fmt.Printf("2*Pi = %g\n", twoPi) // 2*Pi = 6.283185307179586
}

基本类型和运算符

布尔类型 bool:布尔型的值只可以是常量 true 或者 false

数字类型:整型 int 和浮点型 float

格式化说明符:格式化字符串里,%d 用于格式化整数(%x%X 用于格式化 16 进制表示的数字),%g 用于格式化浮点型(%f 输出浮点数,%e 输出科学计数表示法),%0d 用于规定输出定长的整数,其中开头的数字 0 是必须的。

数字值转换:进行a32bitInt = int32(a32Float) 的转换时,小数点后的数字将被丢弃。

复数:Go 拥有以下复数类型:

1
2
complex64 (32 位实数和虚数)
complex128 (64 位实数和虚数)

位运算:

一元运算符:按位补足 ^,位左移 <<,位右移 >>

二元运算符:按位与 &,按位或 |,按位异或 ^,位清除 &^

逻辑运算符:== , != , < , <= , > , >=

算术运算符:常见可用于整数和浮点数的二元运算符有 +-*/

运算符与优先级:

有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:

1
2
3
4
5
6
7
8
优先级   运算符
7 ^ !
6 * / % << >> & &^
5 + - | ^
4 == != < <= >= >
3 <-
2 &&
1 ||

当然,你可以通过使用括号来临时提升某个表达式的整体运算优先级。

类型别名:

1
type TZ int

字符类型:char.go

1
2
3
4
5
6
7
8
9
10
11
12
13
var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3) // UTF-8 code point

输出:
65 - 946 - 1053236
A - β - r
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234

字符串

string 类型的零值为长度为零的字符串,即空字符串 ""

一般的比较运算符(==!=<<=>=>)通过在内存中按字节比较来实现字符串的对比。你可以通过函数len() 来获取字符串所占的字节长度,例如:len(str)

字符串拼接符 +:两个字符串 s1s2 可以通过 s := s1 + s2s += "world" 拼接在一起。

  • 解释字符串:

    该类字符串使用双引号括起来,其中的相关的转义字符将被替换,这些转义字符包括:

    • \n:换行符
    • \r:回车符
    • \t:tab 键
    • \u\U:Unicode 字符
    • \\:反斜杠自身
  • 非解释字符串:

    该类字符串使用反引号括起来,支持换行,例如:

    1
    `This is a raw string \n` 中的 `\n\` 会被原样输出。

时间和日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
import (
"fmt"
"time"
)

var week time.Duration
func main() {
t := time.Now()
fmt.Println(t) // e.g. Wed Dec 21 09:52:14 +0100 RST 2011
fmt.Printf("%02d.%02d.%4d\n", t.Day(), t.Month(), t.Year())
// 21.12.2011
t = time.Now().UTC()
fmt.Println(t) // Wed Dec 21 08:52:14 +0000 UTC 2011
fmt.Println(time.Now()) // Wed Dec 21 09:52:14 +0100 RST 2011
// calculating times:
week = 60 * 60 * 24 * 7 * 1e9 // must be in nanosec
week_from_now := t.Add(week)
fmt.Println(week_from_now) // Wed Dec 28 08:52:14 +0000 UTC 2011
// formatting times:
fmt.Println(t.Format(time.RFC822)) // 21 Dec 11 0852 UTC
fmt.Println(t.Format(time.ANSIC)) // Wed Dec 21 08:56:34 2011
fmt.Println(t.Format("02 Jan 2006 15:04")) // 21 Dec 2011 08:52
s := t.Format("20060102")
fmt.Println(t, "=>", s)
// Wed Dec 21 08:52:14 +0000 UTC 2011 => 20111221
}

指针

程序在内存中存储它的值,每个内存块(或字)有一个地址,通常用十六进制数表示,如:0x6b08200xf84001d7f0

一个指针变量可以指向任何一个值的内存地址 它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址

指针类型前面加上*号(前缀)来获取指针所指向的内容,这里的 * 号是一个类型更改器。使用一个指针引用一个值被称为间接引用。

当一个指针被定义后没有分配到任何变量时,它的值为 nil

一个指针变量通常缩写为 ptr

例:展示了分配一个新的值给 *p 并且更改这个变量自己的值(这里是一个字符串)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
s := "good bye"
var p *string = &s
*p = "ciao"
fmt.Printf("Here is the pointer p: %p\n", p) // prints address
fmt.Printf("Here is the string *p: %s\n", *p) // prints string
fmt.Printf("Here is the string s: %s\n", s) // prints same string
}

输出:
Here is the pointer p: 0x2540820
Here is the string *p: ciao
Here is the string s: ciao

控制结构

if-else 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if condition1 {
// do something
} else if condition2 {
// do something else
}else {
// catch-all or default
}

判断一个字符串是否为空
if str == "" { ... }
if len(str) == 0 {...}

判断系统,分别提示
var prompt = "Enter a digit, e.g. 3 "+ "or %s to quit."

func init() {
if runtime.GOOS == "windows" {
prompt = fmt.Sprintf(prompt, "Ctrl+Z, Enter")
} else { //Unix-like
prompt = fmt.Sprintf(prompt, "Ctrl+D")
}
}


switch 结构

1
2
3
4
5
6
7
8
switch {
case condition1:
...
case condition2:
...
default:
...
}

for结构

基于计数器的迭代

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
for i := 0; i < 5; i++ {
fmt.Printf("This is the %d iteration\n", i)
}
}

for-range 结构

它可以迭代任何一个集合(包括数组和 map),一般形式为:for ix, val := range coll { }

val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值(译者注:如果 val 为指针,则会产生指针的拷贝,依旧可以修改集合中的原值

1
2
3
for pos, char := range str {
...
}

Break 与 continue

break 的作用范围为该语句出现后的最内部的结构,它可以被用于任何形式的 for 循环(计数器、条件判断等)。但在 switch 或 select 语句中,break 语句的作用结果是跳过整个代码块,执行后续的代码。

continue 忽略剩余的循环体而直接进入下一次循环的过程,但不是无条件执行下一次循环,执行之前依旧需要满足循环的判断条件。

标签与 goto

forswitchselect 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:)结尾的单词(gofmt 会将后续代码自动移至下一行)。

1
2
3
4
5
6
7
8
9
LABEL1:
for i := 0; i <= 5; i++ {
for j := 0; j <= 5; j++ {
if j == 4 {
continue LABEL1
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}

函数(function)

函数参数与返回值

按值\引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
fmt.Printf("Multiply 2 * 5 * 6 = %d\n", MultiPly3Nums(2, 5, 6))
// var i1 int = MultiPly3Nums(2, 5, 6)
// fmt.Printf("MultiPly 2 * 5 * 6 = %d\n", i1)
}

func MultiPly3Nums(a int, b int, c int) int {
// var product int = a * b * c
// return product
return a * b * c
}

输出显示:

Multiply 2 * 5 * 6 = 60

命名的返回值

getX2AndX3getX2AndX3_2 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

var num int = 10
var numx2, numx3 int

func main() {
numx2, numx3 = getX2AndX3(num)
PrintValues() //输出:num = 10, 2x num = 20, 3x num = 30
numx2, numx3 = getX2AndX3_2(num)
PrintValues() //输出:num = 10, 2x num = 20, 3x num = 30
}

func PrintValues() {
fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
}

func getX2AndX3(input int) (int, int) {
return 2 * input, 3 * input
}

func getX2AndX3_2(input int) (x2 int, x3 int) {
x2 = 2 * input
x3 = 3 * input
// return x2, x3
return
}

空白符

空白符用来匹配一些不需要的值,然后丢弃掉。ThreeValues 是拥有三个返回值的不需要任何参数的函数,在下面的例子中,我们将第一个与第三个返回值赋给了 i1f1。第二个返回值赋给了空白符 _,然后自动丢弃掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var i1 int
var f1 float32
i1, _, f1 = ThreeValues()
fmt.Printf("The int: %d, the float: %f \n", i1, f1) //输出:The int: 5, the float: 7.500000
}

func ThreeValues() (int, int, float32) {
return 5, 6, 7.5
}

改变外部变量

传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回。如下的例子,reply 是一个指向 int 变量的指针,通过这个指针,我们在函数内修改了这个 int 变量的数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

// this function changes reply:
func Multiply(a, b int, reply *int) {
*reply = a * b
}

func main() {
n := 0
reply := &n
Multiply(10, 5, reply)
fmt.Println("Multiply:", *reply) // Multiply: 50
}

传递变长参数

如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。

示例函数和调用:

1
2
func Greeting(prefix string, who ...string)
Greeting("hello:", "Joe", "Anna", "Eileen")

在 Greeting 函数中,变量 who 的值为 []string{"Joe", "Anna", "Eileen"}

如果参数被存储在一个数组 arr 中,则可以通过 arr... 的形式来传递参数调用变参函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func main() {
x := Min(1, 3, 2, 0)
fmt.Printf("The minimum is: %d\n", x) // 输出:The minimum is: 0
arr := []int{7,9,3,5,1}
x = Min(arr...)
fmt.Printf("The minimum in the array arr is: %d", x) // 输出:The minimum in the array arr is: 1
}

func Min(a ...int) int {
if len(a)==0 {
return 0
}
min := a[0]
for _, v := range a {
if v < min {
min = v
}
}
return min
}

defer 和追踪

https://www.kancloud.cn/kancloud/the-way-to-go/72476

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return 语句同样可以包含一些操作,而不是单纯地返回某个值)。

关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源。

当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)

可以使用 defer 语句实现代码追踪

内置函数

名称 说明
close 用于管道通信
len、cap len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
new、make new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用户内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针(详见第 10.1 节)。它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作(详见第 7.2.3/4 节、第 8.1.1 节和第 14.2.1 节)new() 是一个函数,不要忘记它的括号
copy、append 用于复制和连接切片
panic、recover 两者均用于错误处理机制
print、println 底层打印函数(详见第 4.2 节),在部署环境中建议使用 fmt 包
complex、real imag 用于创建和操作复数(详见第 4.5.2.2 节

递归函数

当一个函数在其函数体内调用自身,则称之为递归。最经典的例子便是计算斐波那契数列,即每个数均为前两个数之和。数列如下所示:

1
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, …
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "fmt"

func main() {
result := 0
for i := 0; i <= 10; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is: %d\n", i, result)
}
}

func fibonacci(n int) (res int) {
if n <= 1 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}

输出:

fibonacci(0) is: 1
fibonacci(1) is: 1
fibonacci(2) is: 2
fibonacci(3) is: 3
fibonacci(4) is: 5
fibonacci(5) is: 8
fibonacci(6) is: 13
fibonacci(7) is: 21
fibonacci(8) is: 34
fibonacci(9) is: 55
fibonacci(10) is: 89

将函数作为参数

函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调 callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
)

func main() {
callback(1, Add)
}

func Add(a, b int) {
fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}

func callback(y int, f func(int, int)) {
f(y, 2) // this becomes Add(1, 2)
}

输出:

The sum of 1 and 2 is: 3

闭包

当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }

这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fplus(3,4)

当然,您也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)

应用闭包:将函数作为返回值

https://www.kancloud.cn/kancloud/the-way-to-go/72481

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func main() {
// make an Add2 function, give it a name p2, and call it:
p2 := Add2()
fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3))
// make a special Adder function, a gets value 3:
TwoAdder := Adder(2)
fmt.Printf("The result is: %v\n", TwoAdder(3))
}

func Add2() func(b int) int {
return func(b int) int {
return b + 2
}
}

func Adder(a int) func(b int) int {
return func(b int) int {
return a + b
}
}

输出:

Call Add2 for 3 gives: 5
The result is: 5

使用闭包调试

能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtimelog 包中的特殊函数来实现这样的功能。包runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候实现一个 where() 闭包函数来打印函数执行的位置:

1
2
3
4
5
6
7
8
9
where := func() {
_, file, line, _ := runtime.Caller(1)
log.Printf("%s:%d", file, line)
}
where()
// some code
where()
// some more code
where()

您也可以设置 log 包中的 flag 参数来实现:

1
2
log.SetFlags(log.Llongfile)
log.Print("")

或使用一个更加简短版本的 where 函数:

1
2
3
4
5
6
7
8
var where = log.Print
func func1() {
where()
... some code
where()
... some code
where()
}

计算函数执行时间

在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 time 包中的 Now()Sub 函数:

1
2
3
4
5
start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)

通过内存缓存来提升性能

进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存。最明显的例子就是生成斐波那契数列的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"time"
)

const LIM = 41

var fibs [LIM]uint64

func main() {
var result uint64 = 0
start := time.Now()
for i := 0; i < LIM; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is: %d\n", i, result)
}
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)
}
func fibonacci(n int) (res uint64) {
// memoization: check if fibonacci(n) is already known in array:
if fibs[n] != 0 {
res = fibs[n]
return
}
if n <= 1 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
fibs[n] = res
return
}

数组与切片

声明和初始化

数组

数组元素可以通过 索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb。

声明的格式是:

1
2
var identifier [len]type
a := [...]string{"a", "b", "c", "d"}

切片

优点 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。

声明切片的格式是: var identifier []type(不需要说明长度)。

一个切片在未初始化之前默认为 nil,长度为 0。

切片的初始化格式是:var slice1 []type = arr1[start:end]

这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。所以 slice1[0] 就等于 arr1[start]

For-range 结构

这种构建方法可以应用与数组和切片:

1
2
3
for ix, value := range slice1 {
...
}

假设我们有如下数组:items := [...]int{10, 20, 30, 40, 50}

1
2
3
for _, item := range items {
item *= 2
}

切片重组(reslice)

切片可以反复扩展直到占据整个相关数组。

示例 7.11 reslicing.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main
import "fmt"

func main() {
slice1 := make([]int, 0, 10)
// load the slice, cap(slice1) is 10:
for i := 0; i < cap(slice1); i++ {
slice1 = slice1[0:i+1]
slice1[i] = i
fmt.Printf("The length of slice is %d\n", len(slice1))
}

// print the slice:
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
}

输出结果:

The length of slice is 1
The length of slice is 2
The length of slice is 3
The length of slice is 4
The length of slice is 5
The length of slice is 6
The length of slice is 7
The length of slice is 8
The length of slice is 9
The length of slice is 10
Slice at 0 is 0
Slice at 1 is 1
Slice at 2 is 2
Slice at 3 is 3
Slice at 4 is 4
Slice at 5 is 5
Slice at 6 is 6
Slice at 7 is 7
Slice at 8 is 8
Slice at 9 is 9

切片的复制与追加

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 函数和向切片追加新元素的 append 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"

func main() {
sl_from := []int{1, 2, 3}
sl_to := make([]int, 10)

n := copy(sl_to, sl_from)
fmt.Println(sl_to)
fmt.Printf("Copied %d elements\n", n) // n == 3

sl3 := []int{1, 2, 3}
sl3 = append(sl3, 4, 5, 6)
fmt.Println(sl3)
}

字符串、数组和切片的应用

  • 7.6.1 从字符串生成字节切片

  • 7.6.2 获取字符串的某一部分

  • 7.6.3 字符串和切片的内存结构

  • 7.6.4 修改字符串中的某个字符

  • 7.6.5 字节数组对比函数

    Compare 函数会返回两个字节数组字典顺序的整数对比结果

  • 7.6.6 搜索及排序切片和数组

    sort 包来实现常见的搜索和排序操作。

  • 7.6.7 append 函数常见操作

  • 我们在第 7.5 节提到的 append 非常有用,它能够用于各种方面的操作:

    1. 将切片 b 的元素追加到切片 a 之后:a = append(a, b...)

    2. 复制切片 a 的元素到新的切片 b 上:

      1
      2
      b = make([]T, len(a))
      copy(b, a)
    3. 删除位于索引 i 的元素:a = append(a[:i], a[i+1:]...)

    4. 切除切片 a 中从索引 i 至 j 位置的元素:a = append(a[:i], a[j:]...)

    5. 为切片 a 扩展 j 个元素长度:a = append(a, make([]T, j)...)

    6. 在索引 i 的位置插入元素 x:a = append(a[:i], append([]T{x}, a[i:]...)...)

    7. 在索引 i 的位置插入长度为 j 的新切片:a = append(a[:i], append(make([]T, j), a[i:]...)...)

    8. 在索引 i 的位置插入切片 b 的所有元素:a = append(a[:i], append(b, a[i:]...)...)

    9. 取出位于切片 a 最末尾的元素 x:x, a = a[len(a)-1], a[:len(a)-1]

    10. 将元素 x 追加到切片 a:a = append(a, x)

  • 7.6.8 切片和垃圾回收

Map

map 是一种特殊的数据结构:一种元素对(pair)的无序集合,pair 的一个元素是 key,对应的另一个元素是 value,所以这个结构也称为关联数组或字典。这是一种快速寻找值的理想结构:给定 key,对应的 value 可以迅速定位。

map 这种数据结构在其他编程语言中也称为字典(Python)、hash 和 HashTable 等。

声明、初始化和 make

map 是引用类型,可以使用如下声明:

1
2
var map1 map[keytype]valuetype
var map1 map[string]int
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"

func main() {
var mapLit map[string]int
//var mapCreated map[string]float32
var mapAssigned map[string]int

mapLit = map[string]int{"one": 1, "two": 2}
mapCreated := make(map[string]float32)
mapAssigned = mapLit

mapCreated["key1"] = 4.5
mapCreated["key2"] = 3.14159
mapAssigned["two"] = 3

fmt.Printf("Map literal at \"one\" is: %d\n", mapLit["one"])
fmt.Printf("Map created at \"key2\" is: %f\n", mapCreated["key2"])
fmt.Printf("Map assigned at \"two\" is: %d\n", mapLit["two"])
fmt.Printf("Map literal at \"ten\" is: %d\n", mapLit["ten"])
}

测试键值对是否存在及删除元素

如果你只是想判断某个 key 是否存在而不关心它对应的值到底是多少,你可以这么做:

1
_, ok := map1[key1] // 如果key1存在则ok == true,否在ok为false

或者和 if 混合使用:

1
2
3
if _, ok := map1[key1]; ok {
// ...
}

for-range 的配套用法

可以使用 for 循环构造 map:

1
2
3
for key, value := range map1 {
...
}

第一个返回值 key 是 map 中的 key 值,第二个返回值则是该 key 对应的 value 值;这两个都是仅 for 循环内部可见的局部变量。其中第一个返回值key值是一个可选元素。如果你只关心值,可以这么使用:

1
2
3
for _, value := range map1 {
...
}

如果只想获取 key,你可以这么使用:

1
2
3
for key := range map1 {
fmt.Printf("key is: %d\n", key)
}

map 类型的切片

假设我们想获取一个 map 类型的切片,我们必须使用两次 make() 函数,第一次分配切片,第二次分配 切片中每个 map 元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"

func main() {
// Version A:
items := make([]map[int]int, 5)
for i:= range items {
items[i] = make(map[int]int, 1)
items[i][1] = 2
}
fmt.Printf("Version A: Value of items: %v\n", items)

// Version B: NOT GOOD!
items2 := make([]map[int]int, 5)
for _, item := range items2 {
item = make(map[int]int, 1) // item is only a copy of the slice element.
item[1] = 2 // This 'item' will be lost on the next iteration.
}
fmt.Printf("Version B: Value of items: %v\n", items2)
}

输出结果:

1
2
Version A: Value of items: [map[1:2] map[1:2] map[1:2] map[1:2] map[1:2]]
Version B: Value of items: [map[] map[] map[] map[] map[]]

需要注意的是,应当像 A 版本那样通过索引使用切片的 map 元素。在 B 版本中获得的项只是 map 值的一个拷贝而已,所以真正的 map 元素没有得到初始化。

map 的排序

map 默认是无序的,不管是按照 key 还是按照 value 默认都不排序

如果你想为 map 排序,需要将 key(或者 value)拷贝到一个切片,再对切片排序(使用 sort 包,详见第 7.6.6 节),然后可以使用切片的 for-range 方法打印出所有的 key 和 value。

将 map 的键值对调

这里倒置是指调换 key 和 value。如果 map 的值类型可以作为 key 且所有的 value 是唯一的,那么通过下面的方法可以简单的做到键值对调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
import (
"fmt"
)

var (
barVal = map[string]int{"alpha": 34, "bravo": 56, "charlie": 23,
"delta": 87, "echo": 56, "foxtrot": 12,
"golf": 34, "hotel": 16, "indio": 87,
"juliet": 65, "kili": 43, "lima": 98}
)

func main() {
invMap := make(map[int]string, len(barVal))
for k, v := range barVal {
invMap[v] = k
}
fmt.Println("inverted:")
for k, v := range invMap {
fmt.Printf("Key: %v, Value: %v / ", k, v)
}
fmt.Println()
}


输出结果:

inverted:
Key: 34, Value: golf / Key: 23, Value: charlie / Key: 16, Value: hotel / Key: 87, Value: delta / Key: 98, Value: lima / Key: 12, Value: foxtrot / Key: 43, Value: kili / Key: 56, Value: bravo / Key: 65, Value: juliet /

包(package)

标准库概述

fmtos 等这样具有常用功能的内置包在 Go 语言中有 150 个以上,它们被称为标准库,大部分(一些底层的除外)内置于 Go 本身。完整列表可以在 Go Walker 查看。

  • unsafe: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。

  • syscall-os-os/exec:

    • os: 提供给我们一个平台无关性的操作系统功能接口,采用类Unix设计,隐藏了不同操作系统间差异,让不同的文件系统和操作系统对象表现一致。

    • os/exec: 提供我们运行外部操作系统命令和程序的方式。

    • syscall: 底层的外部包,提供了操作系统底层调用的基本接口。

    • archive/tar/zip-compress:压缩(解压缩)文件功能。

    • fmt-io-bufio-path/filepath-flag:

      • fmt: 提供了格式化输入输出功能。
      • io: 提供了基本输入输出功能,大多数是围绕系统功能的封装。
      • bufio: 缓冲输入输出功能的封装。
      • path/filepath: 用来操作在当前系统中的目标文件名路径。
      • flag: 对命令行参数的操作。
    • strings-strconv-unicode-regexp-bytes:

      • strings: 提供对字符串的操作。
      • strconv: 提供将字符串转换为基础类型的功能。
      • unicode: 为 unicode 型的字符串提供特殊的功能。
      • regexp: 正则表达式功能。
      • bytes: 提供对字符型分片的操作。
      • index/suffixarray: 子字符串快速查询。
    • math-math/cmath-math/big-math/rand-sort:

      • math: 基本的数学函数。
      • math/cmath: 对复数的操作。
      • math/rand: 伪随机数生成。
      • sort: 为数组排序和自定义集合。
      • math/big: 大数的实现和计算。
    • container-/list-ring-heap: 实现对集合的操作。

      • list: 双链表。
    • time-log:

      • time: 日期和时间的基本操作。
      • log: 记录程序运行时产生的日志,我们将在后面的章节使用它。
    • encoding/json-encoding/xml-text/template:

      • encoding/json: 读取并解码和写入并编码 JSON 数据。
      • encoding/xml:简单的 XML1.0 解析器,有关 JSON 和 XML 的实例请查阅第 12.9/10 章节。
      • text/template:生成像 HTML 一样的数据与文本混合的数据驱动模板(参见第 15.7 节)。
    • net-net/http-html:(参见第 15 章)

      • net: 网络数据的基本操作。
      • http: 提供了一个可扩展的 HTTP 服务器和基础客户端,解析 HTTP 请求和回复。
      • html: HTML5 解析器。
    • runtime: Go 程序运行时的交互操作,例如垃圾回收和协程创建。

    • reflect: 实现通过程序运行时反射,让程序操作任意类型的变量。

regexp 包

下面的程序里,我们将在字符串中对正则表达式进行匹配。

如果是简单模式,使用 Match 方法便可:

1
ok, _ := regexp.Match(pat, []byte(searchIn))

变量 ok 将返回 true 或者 false,我们也可以使用 MatchString

1
ok, _ := regexp.MathString(pat, searchIn)

更多方法中,必须先将正则通过 Compile 方法返回一个 Regexp 对象。然后我们将掌握一些匹配,查找,替换相关的功能。

示例 9.2 pattern.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main
import (
"fmt"
"regexp"
"strconv"
)
func main() {
//目标字符串
searchIn := "John: 2578.34 William: 4567.23 Steve: 5632.18"
pat := "[0-9]+.[0-9]+" //正则

f := func(s string) string{
v, _ := strconv.ParseFloat(s, 32)
return strconv.FormatFloat(v * 2, 'f', 2, 32)
}

if ok, _ := regexp.Match(pat, []byte(searchIn)); ok {
fmt.Println("Match Found!")
}

re, _ := regexp.Compile(pat)
//将匹配到的部分替换为"##.#"
str := re.ReplaceAllString(searchIn, "##.#")
fmt.Println(str)
//参数为函数时
str2 := re.ReplaceAllStringFunc(searchIn, f)
fmt.Println(str2)
}

输出结果:

Match Found!
John: ##.# William: ##.# Steve: ##.#
John: 5156.68 William: 9134.46 Steve: 11264.36

锁和 sync 包

精密计算和 big 包

自定义包和可见性

为自定义包使用 godoc

使用 go install 安装自定义包

自定义包的目录结构、go install 和 go test

通过 Git 打包和安装

Go 的外部包和项目

结构(struct)与方法(method)

结构体定义(struct)

结构体定义的一般方式如下:

1
2
3
4
5
type identifier struct {
field1 type1
field2 type2
...
}

type T struct {a, b int} 也是合法的语法,它更适用于简单的结构体。

https://www.kancloud.cn/kancloud/the-way-to-go/72512

使用工厂方法创建结构体实例

Go 语言不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在 Go 中实现 “构造子工厂“ 方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以 new 或 New 开头。假设定义了如下的 File 结构体类型:

1
2
3
4
type File struct {
fd int // 文件描述符
name string // 文件名
}

下面是这个结构体类型对应的工厂方法,它返回一个指向结构体实例的指针:

1
2
3
4
5
6
7
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}

return &File(fd, name)
}

然后这样调用它:

1
f := NewFile(10, "./test.txt")

在 Go 语言中常常像上面这样在工厂方法里使用初始化来简便的实现构造子。

如果 File 是一个结构体类型,那么表达式 new(File)&File{} 是等价的。

这可以和大多数面向对象编程语言中笨拙的初始化方式做个比较:File f = new File(...)

我们可以说是工厂实例化了类型的一个对象,就像在基于类的OO语言中那样。

如果想知道结构体类型T的一个实例占用了多少内存,可以使用:size := unsafe.Sizeof(T{})

使用自定义包中的结构体

下面的例子中,main.go 使用了一个结构体,它来自 struct_pack 下的包 structPack。

示例 10.5 structPack.go:

1
2
3
4
5
6
package structPack

type ExpStruct struct {
Mi1 int
Mf1 float32
}

示例 10.6 main.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
"./struct_pack/structPack"
)

func main() {
struct1 := new(structPack.ExpStruct)
struct1.Mi1 = 10
struct1.Mf1 = 16.

fmt.Printf("Mi1 = %d\n", struct1.Mi1)
fmt.Printf("Mf1 = %f\n", struct1.Mf1)
}

输出:

Mi1 = 10
Mf1 = 16.000000

带标签的结构体

结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。我们将在下一章(第 11.10 节)中深入的探讨 reflect包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。

示例 10.7 struct_tag.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"reflect"
)

type TagType struct { // tags
field1 bool "An important answer"
field2 string "The name of the thing"
field3 int "How much there are"
}

func main() {
tt := TagType{true, "Barak Obama", 1}
for i := 0; i < 3; i++ {
refTag(tt, i)
}
}

func refTag(tt TagType, ix int) {
ttType := reflect.TypeOf(tt)
ixField := ttType.Field(ix)
fmt.Printf("%v\n", ixField.Tag)
}

输出:

An important answer
The name of the thing
How much there are

匿名字段和内嵌结构体

结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体

可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为。Go 语言中的继承是通过内嵌或组合来实现的,所以可以说,在 Go 语言中,相比较于继承,组合更受青睐。

匿名结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import "fmt"

type innerS struct {
in1 int
in2 int
}

type outerS struct {
b int
c float32
int // anonymous field
innerS //anonymous field
}

func main() {
outer := new(outerS)
outer.b = 6
outer.c = 7.5
outer.int = 60
outer.in1 = 5
outer.in2 = 10

fmt.Printf("outer.b is: %d\n", outer.b)
fmt.Printf("outer.c is: %f\n", outer.c)
fmt.Printf("outer.int is: %d\n", outer.int)
fmt.Printf("outer.in1 is: %d\n", outer.in1)
fmt.Printf("outer.in2 is: %d\n", outer.in2)

// 使用结构体字面量
outer2 := outerS{6, 7.5, 60, innerS{5, 10}}
fmt.Printf("outer2 is:", outer2)
}

输出:

outer.b is: 6
outer.c is: 7.500000
outer.int is: 60
outer.in1 is: 5
outer.in2 is: 10
outer2 is:{6 7.5 60 {5 10}}

通过类型 outer.int 的名字来获取存储在匿名字段中的数据,于是可以得出一个结论:在一个结构体中对于每一种数据类型只能有一个匿名字段。

内嵌结构体

同样地结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用,如同上面例子中那样。外层结构体通过outer.in1 直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。内层结构体被简单的插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type A struct {
ax, ay int
}

type B struct {
A
bx, by float32
}

func main() {
b := B{A{1, 2}, 3.0, 4.0}
fmt.Println(b.ax, b.ay, b.bx, b.by)
fmt.Println(b.A)
}

输出:

1 2 3 4
{1 2}

方法(method)

垃圾回收和 SetFinalizer

接口(interface)与反射(reflection)

接口是什么

接口嵌套接口

类型断言:如何检测和转换接口变量的类型

类型判断:type-switch

测试一个值是否实现了某个接口

使用方法集与接口

第一个例子:使用 Sorter 接口排序

第二个例子:读和写

空接口

反射包

Go 高级编程

读写数据

读取用户的输入

文件读写

文件拷贝

从命令行读取参数

用buffer读取文件

用切片读写文件

用 defer 关闭文件

使用接口的实际例子:fmt.Fprintf

Json 数据格式

XML 数据格式

用 Gob 传输数据

Go 中的密码学

错误处理与测试

错误处理

运行时异常和 panic

从 panic 中恢复(Recover)

自定义包中的错误处理和 panicking

一种用闭包处理错误的模式

启动外部命令和程序

Go 中的单元测试和基准测试

测试的具体例子

用(测试数据)表驱动测试

性能调试:分析并优化

协程(goroutine)与通道(channel)

并发、并行和协程

使用通道进行协程间通信

协程同步:关闭通道-对阻塞的通道进行测试

使用 select 切换协程

通道,超时和计时器(Ticker)

协程和恢复(recover)

网络、模版与网页应用

tcp服务器

一个简单的web服务器

访问并读取页面数据

写一个简单的网页应用

实际应用

常见的陷阱与错误

误用短声明导致变量覆盖

误用字符串

发生错误时使用defer关闭一个文件

不需要将一个指向切片的指针传递给函数

使用指针指向接口类型

使用值类型时误用指针

误用协程和通道

闭包和协程的使用

糟糕的错误处理

模式

关于逗号ok模式

出于性能考虑的实用代码片段

字符串

数组和切片

映射

结构体

接口

函数

文件

协程(goroutine)与通道(channel)

网络和网页应用

其他

出于性能考虑的最佳实践和建议

作者

Se7en

发布于

2021-09-27

更新于

2022-05-05

许可协议

评论