简明 Go 教程

简明 Go 教程

安装以及运行

关于如何安装 Go ,官网的教程已经非常详细。如果困惑于 GOPATH 的设置,可以使用 JetBrains 家的 Gogland。 只需要将 GOPATH 放在想放的目录,然后在 Gogland 选择上全局 GOPATH 路径即可,剩下的就不去烦心了,同时 Project 可以放在任何地方,而不是考虑 Project 需要放在 src 下面。在 Gogland 中有常见的两种运行方式,一种是直接使用来 go run 来直接运行 .go文件,还有一种是先 build 然后再 run。

这里有必要提一下 Build 这种方式的运行。正如 Gogland 的 Run/Debug Configurations 中所提到的,Build 会把程序作为一个 Application 来对待,build 命令只生成可执行的文件。

和 C 的思想有点像,如果要生成可执行程序,必须建一个 main 的 package,同时这个 package 中必须包含一个 main()。另外同一个路径下只能存在一个 package 。

所以一个最基础的 hello world 是这个样子的

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Printf("hello, world\n")
}

基础语法

基本类型

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

和 C 语言不同, Go 的在不同类型之间的变量赋值时需要显式转型。

变量

Go 定义变量类似 JS 使用 var 关键词,但是在变量名之后需要添上变量类型,在一行中定义多个变量时,最后的类型可代表这多个变量都是这种类型。在定义变量的同时,也可以初始化变量。

1
2
3
var i int
var c, python, java bool // 这三个变量都是 bool 类型
var i, j int = 1, 2 // 初始化 i 为 1, j 为 2

在明确的变量类型赋值时,可以使用 := 进行简写,Go 会自动推导出变量的类型。 比如 k := 3 就等价于 var k int = 3

定义变量但没初始化时,变量会默认为 零值

常量

常量的定义与变量类似,只不过使用 const 关键字。常量可以是字符、字符串、布尔或数字类型的值。常量不能使用 := 语法定义。一个未指定类型的常量由上下文来决定其类型。

函数

使用 func 关键词来定义函数,这里要注意的是,和 C 不同,参数是先写参数名,再写参数类型。在这之后跟上返回值类型。比如接受两个 int 类型的参数的 add 函数,

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

参数也可以被缩写成 add(x, y int)

Go 和 Python 类似,支持返回多个值,所以在写返回值类型时,用括号将多个返回值的类型括起来即可。

Go 的返回值可以被命名,这样提高了代码的可读性,在规定返回值类型的地方填上要返回的变量,这样函数体中只需要些上 return 而不需要在最后一行写上需要返回的变量名。

Go 的核心是包,每个 Go 程序都是由包组成的。程序运行的入口是包 main。导入包的形式有两种,一种是

1
2
import "fmt"
import "math"

还有一种是

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

在 Go 中,首字母大写的名称是被导出的。因此可以通过 math.Pi 来调用 math 包中的 Pi 常量。

if

if 循环和 C 很像,只是没有了括号,但一定要加上 {} 的。 Go 的 if 有个特殊用法,就是在判断之前先执行一条语句,当然这条语句产生的变量它的作用域只能是 if 中,在 if 外是无法访问。

switch

switch 功能类似于 C 的switch,它是一个结构体(struct)就是一个字段的集合。除非以 fallthrough 语句结束,否则分支会自动终止。switch 默认相当于每个 case 最后带有 break,匹配成功后不会自动向下执行其他 case。

for

for 和 if 的约束类似,比如 1 加到 100

1
2
3
4
sum := 0
for i := 0; i < 100; i++ {
sum += i
}

Go 的另一种 for 循环很像 C 中的 while

1
2
3
4
sum := 1
for sum < 1000 {
sum += sum
}

defer

defer 语句会延迟函数的执行直到上层函数返回。

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

fmt.Println("hello")
}

上面 defer 语句只有在 main() 即将执行完才会运行。

当一个函数体中有多个 defer ,遵从后进先出的原则(和堆栈一样),对被延迟函数进行调用。

指针

指针和 C 一样, & 是取地址, * 是解引用。

结构体

同样和 C 语言一样,一般用 type 结构名 struct,通过 . 可以访问结构体里的字段。这里有个细节是,如果直接输出结构体,会有 {} 包住内含的字段,字段之间用空格分割。

数组

[n]T 代表 nT 类型的数组。要注意的是数组的长度是不能随意改变的。在定义的时候,如果直接进行初始化赋值,可以省略 n 让 Go 自行判断。

数组可以进行切片,切片和 Python 有点类似,但 Go 切片不能从尾部开始切,也就是说索引必须是正的。 Go 的切片是得到的原数组的引用,索引修改切片对象就是修改了原数组。

创建数组也可用 make() 来构造。 len() 是长度, cap 是容量。关于容量这个概念,其他很多语言都没有。长度是指已经被赋过值的最大下标+1,可通过内置函数len()获得。容量是指切片目前可容纳的最多元素个数,可通过内置函数cap()获得。切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。一般数组的 len 和 cap 是一样的。

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

func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
a := s[3:6]
b := a[1:2]
printSlice(a) // len=3 cap=7 [3 4 5]
printSlice(b) // len=1 cap=6 [4]
}

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

a, b 的容量比长度都要大,这里cap的改变规则为原cap值减掉起始下标。

使用 append 可以对切片进行添加。

关于切片,细节可以看Slices: usage and internals

Maps

这里的 map 和 Python 中不同,这个有点像 Python 中的 dict。 map 需要用 make 初始化其对应类型。

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

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"])
}

语法上类似 struct,可以省去 make ,直接对 map 进行赋值。

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

或者

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

通过上面几段代码可以感受到, map 不算是一个函数,更像是一种类型。map 后面加 [] 之后加上返回类型,括号里的是键的类型。

对于 map 的插入和更新,和使用 Python 的 dict 类似。 对于访问 map ,当访问的键不存在时,默认值是0,其实在访问键时,map 会返回两个值,第一个值是键对应的值,第二个值是这个键是否在 map 中。

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

func main() {
m := make(map[string]int)

m["Answer"] = 42
fmt.Println("The value:", m["Answer"])

m["Answer"] = 48
fmt.Println("The value:", m["Answer"])

delete(m, "Answer")
fmt.Println("The value:", m["Answer"])

v, ok := m["Answer"]
fmt.Println("The value:", v, "Present?", ok)
}

Range

Range 是对于切片或者 map 的迭代。

1
2
3
4
5
6
7
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}

闭包

通俗的说,闭包就是函数中包含了一些内部变量,闭包函数 return 的也是一个函数,在这个返回函数中需要用到闭包函数里存放的变量。

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

func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}

上面的这个例子就是 adder() 函数中有一个变量 sum,可以被 adder() 返回的函数所访问修改,函数外部是看不到这个 sum 变量的。

比如基于闭包的斐波那契数列就可以这样写

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

func fibonacci() func() int {
var a, b int = 0, 1

return func() int {
ret := a
a, b = b, a+b
return ret
}
}

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

方法

Go 是没有类这种说法的,所以可以通过在函数上指定一个接收者将一个方法与一个类型绑定。其实类似于 Python 的类函数,Python 通过 self 变量将自身的类进行传参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}

当然也可以在声明常规函数的时候将接收者作为参数传入以实现同样的绑定效果。方法不一定要与结构体绑定,也可以与一个变量类型绑定。

指针接收者

这里有个细节,当指针作为接收者时,调用这个方法会对这个类型进行操作,而不是对这个类型的副本进行操作。另外,当变量是地址时,调用的方法的接收者为值类型是,Go 会自动解引用。

使用指针接收者来声明方法,可以避免每个调用方法时的拷贝值的损耗,这一点尤其在接收者是个非常大的结构体时。

接口

接口就是包含一组方法的标记。接口的值可以是任何实现这个方法的值。接口是隐性规定了相关类型必须要实现接口里的方法。这样做的使程序能够解耦并且很规范。

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

type I interface {
M()
}

type T struct {
S string
}

// This method means type T implements the interface I,
// but we don't need to explicitly declare that it does so.
func (t T) M() {
fmt.Println(t.S)
}

func main() {
var i I = T{"hello"}
i.M()
}

下面这个程序重点在于方法的类型,会发现输出的类型都会以 main.作为前缀,比如第一个的输出是 (&{Hello}, *main.T) ,第二个是 (3.141592653589793, main.F)

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
import (
"fmt"
"math"
)

type I interface {
M()
}

type T struct {
S string
}

func (t *T) M() {
fmt.Println(t.S)
}

type F float64

func (f F) M() {
fmt.Println(f)
}

func main() {
var i I

i = &T{"Hello"}
describe(i)
i.M()

i = F(math.Pi)
describe(i)
i.M()
}

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

若变量的方法是 nil ,在调用接口规定的方法时,是以nil作为接收者,其值为 。若变量本身没有值,则调用方法时会出错。

这里有一种操作是空接口,其类型是任意类型。空接口被用来处理不知道类型的值。

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

func main() {
var i interface{}
describe(i)

i = 42
describe(i)

i = "hello"
describe(i)
}

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

参考

  1. golang之package
  2. Go中的switch fallthrough
  3. 老虞学GoLang笔记-数组和切片