包、变量和函数

1
2
3
4
5
package main
import "fmt"
func main() {
fmt.Println("hello world")
}

每个Go程序都是由包构成。

程序从main包开始运行。

导入包

分组形式导入包(推荐)

1
2
3
4
import (
"fmt"
"math"
)

编写多个导入语句

1
2
import "fmt"
import "math"

导出名

名字以大写字母开头是导出名,如Println。未导出的名字在该包外无法访问。

函数

类型在变量名之后。

1
2
3
func add(x int, y int) int {
return x + y
}

当连续两个或多个函数的已命名形参类型相同时,可以只写最后一个的类型。

1
2
3
func add(x, y int) int {
return x + y
}

与上面的例子一样。

多值返回

1
2
3
func swap(x, y string) (string, string) {
return y, x
}

命名返回值

返回值可以被命名,它们被视作定义在函数顶部的变量。

返回值名称应当具有一定意义,它可以作为文档使用。

没有参数的return语句返回已命名的返回值,也就是直接返回。

直接返回语句应当仅用在下面的短函数中,长函数会影响代码可读性。

1
2
3
4
5
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return //等同于return x,y
}

变量

var语句用于声明一个变量列表。var语句可以出现在包或者函数级别。

1
2
3
4
5
6
var c, python, java bool

func main() {
var i int
fmt.Println(i, c, python, java)
}
1
0 false false false

初始化

变量声明可以包含初始值,每个变量对应一个。

如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。

1
2
3
4
5
6
var i, j int = 1, 2

func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
1
1 2 true false no!

短变量声明

在函数中,简介赋值语句:=可代替var

在函数外必须以关键字(varfunc等等)开始,不能用:=

1
2
3
4
5
6
func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
fmt.Println(i, j, k, c, python, java)
}

基本类型

基本类型有

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

变量声明可以和导入语句一样“分组”成一个语法块。

1
2
3
4
5
6
7
8
9
10
11
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)

func main() {
fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
fmt.Printf("Type: %T Value: %v\n", z, z)
}
1
2
3
Type: bool Value: false
Type: uint64 Value: 18446744073709551615
Type: complex128 Value: (2+3i)

零值

没有明确初始值的变量声明被赋予零值。

数值类型为0,布尔类型为false,字符串为""(空字符串)。

类型转换

表达式 T(v) 将值 v 转换为类型 T。Go 在不同类型的项之间赋值时需要必须显式转换。

一些关于数值的转换:

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

或者,更加简单的形式:

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

类型推导

声明变量而不指定其类型时(即不带类型的:=var = ),变量类型由右值推导得出。

当右值声明了类型时,新变量的类型与其相同:

1
2
var i int
j := i //j也是一个int

当右值没有明确类型时,新变量的类型取决于右值常量的精度:

1
2
3
i := 42           // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128

常量

使用const关键字。

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

1
const Pi = 3.14

字符串操作

标准库strings里面有很多常用的字符串工具函数。比如contains判断一个字符串里面是否有包含另一个字符串,count,index查找某个字符串位置,join连接多个字符串。

字符串格式化

可以用标准fmt库里面的相关方法。比如用Printf时可以用%v来打印任何类型的变量,用%+v打印详细结果。

流程控制语句:for、if、else、switch和defer

for

无序小括号

1
2
3
4
5
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum) //45

for是Go中的“while”

去掉分号的for

1
2
3
4
5
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum) //1024

无限循环

省略循环条件

1
2
for {
}

if

无需小括号

if简短语句

for一样,if可以在条件表达式前执行一个简单的语句,该语句声明的变量作用域仅在if之内。

1
2
3
4
5
6
7
8
9
10
11
12
13
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}

func main() {
fmt.Println(
pow(3, 2, 10),
pow(3, 3, 20),
)
}
1
9 20

if和else

switch

Go 只运行选定的 case,而非之后所有的 case。Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束。

Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。

1
2
3
4
5
6
7
8
9
10
11
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}

无条件的switch

switch true

1
2
3
4
5
6
7
8
9
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}

defer

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

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

1
2
3
4
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
1
2
hello
world

defer栈

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

1
2
3
4
5
6
7
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
1
2
3
4
5
6
counting
done
9
8
7
...

更多类型:struct、slice和映射

指针

类型*T是指向T类型值的指针。其零值为nil

1
var p *int

&操作符会生成一个指向其操作数的指针。

1
2
i := 42 
p = &i

*操作符表示指针指向的底层值。

1
2
fmt.Println(*p) // 通过指针p读取i
*p = 21 //通过指针p设置i

结构体

一个结构体(struct)就是一组字段(field)。

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

func main() {
fmt.Println(Vertex{1, 2})
}
1
{1 2}

结构体字段

结构体字段使用.来访问。

1
v.X = 4

结构体指针

通过(*p).X来访问其字段,也可以隐式间接引用p.X

1
2
3
v := Vertex{1, 2}
p := &v
p.X = 1e9

结构体用法

结构体通过直接列出字段的值来新分配一个结构体。

使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)

特殊的前缀 & 返回一个指向结构体的指针。

1
2
3
4
5
6
7
8
9
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

JSON处理

对于struct类型,可以用encoding/json包里面的json.Marshaler来序列化或者反序列化。

数组

表达式

1
var a [10]int

切片

切片为数组提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

形式:[]T

切片通过上届和下界来界定:a[low : high],左闭右开

1
2
3
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)

切片就像数组的引用

切片不储存任何数据,它只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。

与它共享底层数组的切片都会观测到这些修改。

切片用法

类似于没有长度的数组。

1
[]bool{true, true, false}

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

1
2
var a [10]int 
a[0:10] //等价于a[:10]、a[0:]和a[:]

切片的长度与容量

假设有切片s

长度:它所包含的元素个数。通过len(s)获取。

容量:从它的第一个元素开始数,到其底层数组元素末尾的个数。通过cap(s)获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s) //len=6 cap=6 [2 3 5 7 11 13]
// 截取切片使其长度为 0
s = s[:0]
printSlice(s) //len=0 cap=6 []
// 拓展其长度
s = s[:4]
printSlice(s) //len=4 cap=6 [2 3 5 7]
// 舍弃前两个值
s = s[2:]
printSlice(s) //len=2 cap=4 [5 7]
}

func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

nil切片

切片的零值是nil,nil切片的长度和容量为0且没有底层数组。

1
var s []int

用make创建切片

切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:

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

要指定它的容量,需向 make 传入第三个参数:

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

切片的切片

1
2
3
4
5
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}

向切片添加元素

append函数。

原型:

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

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

s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。

1
2
3
4
5
6
7
var s []int //len=0 cap=0 []
// 添加一个空切片
s = append(s, 0) //len=1 cap=1 [0]
// 这个切片会按需增长
s = append(s, 1) //len=2 cap=2 [0 1]
// 可以一次性添加多个元素
s = append(s, 2, 3, 4) //len=5 cap=6 [0 1 2 3 4]

range

for 循环的 range 形式可遍历切片或映射。

当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

1
2
3
4
var pow = []int{1, 2, 4, 8}
for i, v := range pow {
fmt.Printf("下标:%d,对应元素:%d\n", i, v) //0,1 1,2 2,4 3,8
}

可以将下标或值赋予 _ 来忽略它。

1
2
for i, _ := range pow
for _, value := range pow

若只需要索引,忽略第二个变量即可。

1
for i := range pow

map映射

映射将键映射到值。映射的零值为 nilnil 映射既没有键,也不能添加键。make 函数会返回给定类型的映射,并将其初始化备用。

1
2
3
4
5
6
7
8
9
10
11
type Vertex struct {
Lat, Long float64
}
var m map[string]Vertex
func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"]) //{40.68433 -74.39967}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
func main() {
fmt.Println(m) //map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
fmt.Println(m["Google"]) //{37.42202 -122.08408}
}

若顶级类型只是一个类型名,你可以在文法的元素中省略它。

1
2
3
4
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}

修改映射

在映射 m 中插入或修改元素:

1
m[key] = elem

获取元素:

1
elem = m[key]

删除元素:

1
delete(m, key)

通过双赋值检测某个键是否存在:

1
elem, ok = m[key]

keym 中,oktrue ;否则,okfalse。若 key 不在映射中或者不存在,那么 elem 是该映射元素类型的零值。

:若 elemok 还未声明,可以使用短变量声明:

1
elem, ok := m[key]

函数值

函数也是值。它们可以像其它值一样传递。函数值可以用作函数的参数或返回值。

1
2
3
4
5
6
7
8
9
10
11
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}

方法和接口

方法

Go 没有类。不过你可以为结构体类型定义方法。方法就是一类带特殊的 接收者 参数的函数。方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

1
2
3
4
5
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
...
fmt.Println(v.Abs()) //调用方法

在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。

方法只是个带接收者参数的函数。

1
2
3
4
5
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
...
fmt.Println(Abs(v)) //调用函数

在为非结构体类型声明方法时,接收者的类型定义和方法声明必须在同一包内;不能为基本类型声明方法。

1
2
3
4
5
6
7
type MyFloat float64
func (f MyFloat) Abs() float64 { //不能直接func (f float64) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

指针接收者

可以为指针接收者声明方法。同样,接受者指针指向的类型不能是基本类型。指针用的更多。

1
func (v *Vertex) Scale(f float64) {

以指针为接收者的方法被调用时,接收者既能为值又能为指针:

1
2
3
4
5
6
7
8
func (v *Vertex) Scale(f float64) {
...
}
...
var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK

Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)

同理,而以值为接收者的方法被调用时,接收者既能为值又能为指针。

接口

错误

用一个函数来处理错误。错误处理是可以自定义的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type MyError struct {
When time.Time
What string
}
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}
func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}

并发

goroutine(Go程)

goroutine是由Go运行时管理的轻量级线程。