GO学习笔记

Go 语言学习笔记,内容来源于官方文档,翻译质量较差会逐步改进。

概述

  • Go 的开发人员通常保存所有的单位在一个工作空间。
  • 一个工作空间包含多个存在版本控制的仓库(例如被 git 管理的仓库)。
  • 每一个仓库有一个或者多个包。
  • 每一个包都是一个目录,目录中包含一个或者多个 Go 源文件。
  • 包的路径直接决定了它的引入路径。

Workspaces

工作空间是一个包含下列两个目录的层次结构:

  1. src 包含了 Go 的源代码
  2. bin 包含了可执行文件

GOPATH 环境变量

GOPATH 指定了你的工作空间所在的位置。默认是一个叫做 go 的文件夹,在 UNIX 下的路径为 $HOME/go, 在 Plan 9 的路径为 $Home/go, Windows下通常是 %USERPROFILE%\go。

如果你要自定义你的工作空间,可以通过设置 GOPATH 环境变量来完成。

import paths

import paths 是用于唯一标识包的字符串。一个包的 import path 和它在本地工作空间和远程仓库中的位置相关。

标准库中的 import path 一般采用缩写,例如 “fmt” 和 “net/http”。对于你自己的包,你要选择一个 base path,来避免和未来的标准库和其他的包产生冲突。

包名

Go 源码文件第一行比如是包名

1
package name

按照约定,包名与导入路径的最后一个元素一致。

为了方便,Go 将 import path 的最后一个元素设置为包名,例如你的包被引入为“crypto/rot13”,则包名就是rot13。

Remote packages

import path 也可以是从远程仓库获取的路径。go 工具会根据这个地址自动的从远程仓库抓取这些包。

如果包不存在,则会用 go get 会自动执行,并将其放到指定的工作空间中(即设置的 GOPATH)。

导出名

在 Go 中, 如果一个名字以大写字母开头,那么它就是导出的。

例如,Pizza 就是个已导出名,Pi 也同样,它导出自 math 包。

在导入一个包时,你只能引用其中已导出的名字。任何“未导出”的名字在该包外均无法访问。

函数

Go 语言中函数的类型在变量名之后。

当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。

例如:

1
2
3
X int, y int
// 可以缩写为
x, y int

Go 语言中,函数可以返回任意数量的返回值,下列的 swap 函数返回了两个字符串。

1
2
3
4
5
6
7
package main

import "fmt"

func swap(x, y string) (string, string) {
return y, x
}

Go 的返回值可以被命名,他们是定义在函数顶部的变量,也就是第二个括号就是返回值的信息,没有参数的 return 返回已命名的返回值,也就是直接返回。例如:

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

import "fmt"

func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}

func main() {
fmt.Println(split(17))
}

变量

Go 语言中使用 var 来声明变量。

在函数中,可以使用 := 在类型明确的地方代替 var 声明。

注意:函数外的每个语句都必须以关键字开始(例如:var func),因此 := 方式不能再函数外使用。

基本类型

Go 的基本类型有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
// 表示一个 Unicode 码点

float32 float64

complex64 complex128

int, uintuintptr 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。

类型转换

Go 在不同类型的项之间赋值时需要显式转换。

1
2
3
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

在声明一个变量而不指定其类型时,变量的类型由右值推到而出。

常量

Go 语言中,使用 const 来声明常量。

常量可以是字符、字符串、布尔值或数值。

常量不能用 := 语法声明。

数值常量是高精度的 值。

一个未指定类型的常量由上下文来决定其类型。

流程控制语句

for

Go 只有一种循环结构:for循环

基本的for循环由三个部分组成,

  • 初始化语句:第一次迭代执行前
  • 条件表达式:每次迭代前求值
  • 后置语句:每次的迭代后执行

其中条件表达式是必须的,初始化语句和后置语句都是可选的

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

import "fmt"

func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}

Go 语言中没有 while,可以通过for来实现while

1
2
3
4
for exp {
// do something
// exit
}

上述结构中,不写exp,就可以构成无限循环

if

if 表达式外不需要小括号 ( ),但大括号 { } 是必须的。

与其他语言不同的是,if 可以再条件表达式前执行一个简单的语句。

1
2
3
4
5
6
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}

switch

Go 语言的 switch 只运行选定的 case,即默认每个 case 后都有一个 break 语句。

switch 的 case 无需为常量,并且取值不必为整数。

switch 的 case 求值顺序是从上到下,至到匹配成功停止。

defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

指针

指针保存了值的内存地址。零值为 nil。

与 C 不同, Go 中没有指针运算。

结构体

一个结构体是一租字段,Go 中没有类的概念。

1
2
3
4
5
package main

type Vertx struct {
x, y int
}

如果有一个指向结构体的指针 p,那么可以通过 (*p).x 来访问其字段 x,为了方便,Go 语言允许我们使用隐式调用,即 p.x 就可以访问。

可以通过结构体文法来直接分配一个新的结构体。

1
2
3
4
5
6
7
8
type Vertex struct {
X, Y int
}

v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)

数组

1
var array [10]int

上述表达式表示,将变量声明为拥有10个整数的数组

注意:数组的长度是其类型的一部分,声明后数组长度不能改变。

切片

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

类型 []T 表示一个元素类型为 T 的切片。

切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:

1
a[low : high]

它会选择一个半开区间,包括第一个元素,但排除最后一个元素。

切片类似数组的引用,切片不存储任何数据,更改切片的元素会影响底层数组。

新建切片时,可以利用默认行为来忽略上下界。

切片的下界的默认值为0,切片上界的默认值为该切片的长度。

切片拥有 长度容量,注意,这是两个不同的概念。

切片的长度是它,即该切片所包含的元素个数。

切片的容量是从它的第一个元素开始,到底层数组末尾的长度。

切片 S 的长度和容量可以通过表达式 len(s) 和 cap(s) 来获取。

切片的零值是 nil。

使用 make 创建切片

切片可以使用内置函数 make 来创建,这也是创建动态数组的方式。

make 函数会分配一个元素为零值的数组并返回一个引用了它的切片

1
2
3
4
5
var name = make(切片类型, 长度, 容量)

a := make([]int, 5) // len(a) = 5

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

切片可以包含任何类型,包括其他切片(实际操作未理解如何使用)

向切片追加元素

可以使用 append 函数来为切片追加新的元素。

1
func append(s []T, vs ... T) [] T

append 第一个参数 s 是元素类型为 T 的切片,其余类型为 T 的值会追加到该切片的末尾。

append 的返回值是一个包含原有切片所有元素加上新添加元素的切片。

如果 s 的底层数组太小,append 就会重新分配一个更大的底层数组,返回的切片会指向这个新的数组。

Range

for 循环中可以使用 range 的形式来遍历切片或映射。

1
2
3
for index, value range array {
// do something
}

range 再每次迭代是会返回两个值,第一个是当前元素下标,第二个值是对应元素的一份副本。

可以是用 _ 来忽略 range 返回的元素下标或者元素的值。

映射

即键值对。

映射的零值为 nil 。nil 映射既没有键,也不能添加键。

make 函数会返回给定类型的映射,并将其初始化备用。

1
2
3
4
5
6
var m map[string]Vertex

m = make(map[string]Vertex)
m["Bell Labs"] = Vertex {
40.68433, -74.39967,
}

映射的文法和结构体类似,但必须有键名。

基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var elem T
var ok bool
// 向映射中插入或修改元素
map[key] = elem

// 获取元素
elem = map[key]

// 删除元素
delete(m, key)

// 双检测法,检测某个键是否存在
elem, ok = map[key]

if ok {
// key exist
}

方法

Go 中 函数和方法看似很像,但实际引用场景不一样。

Go 中没有类吗,但可以为结构体类型定义方法。

方法实际上就是函数,是一类带有特殊的 接受者 参数的函数。

1
2
3
4
5
6
7
type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

方法也可以为非结构体类型声明方法,但要注意的是,就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。

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

import (
"fmt"
"math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}

指针接受者

方法的接受者可以是类型 T,也可以是类型T的指针 *T。

若使用值接收者,那么方法会对原始值的副本进行操作。(对于函数的其它参数也是如此。)方法必须用指针接受者来更改 main 函数中声明的值。

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
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}

接口

接口类型是一组有方法签名定义的集合

接口类型的变量可以保存任何实现了这些方法的值。

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"

type Abser interface {
Abs() float64
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

func main() {
var a Abser
f := MyFloat(24.0)
a = f // 因为 MyFloat实现了Abser接口

fmt.println(a.Abs())
}

上述类型为 MyFloat 的类型实现了 Abser 接口。

接口与隐式实现

自定义的类型(结构体,非结构体)通过实现一个接口的 所有方法 来实现该接口。既然无需任何的显式声明,也就不用 “implement” 关键字来标识。

接口值

接口也是值,它们可以像其他值一样传递

接口值可以用作函数的参数或返回值。

在内部,接口值可以看做包含值和类型的元组

1
(value, type)
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

type I interface {
M ()
}

func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}