Go语言简明教程

本文旨在最短的时间内了解Go语言,适用于有其他语言基础,想迅速入门的同学。

1. 下载安装

到Go官网:https://golang.org/下载安装包:

或者Mac的同学可以直接使用brew命令来安装:

1
2
brew update
brew install golang

创建一个workspace folder,通常放在$Home目录下

1
mkdir $HOME/<your_go_workspace_folder_name>

添加环境变量:

编辑~/.zshrc,添加如下内容:

1
2
3
4
export GOPATH=$HOME/<your_go_workspace_folder_name>
export GOROOT=/usr/local/opt/go/libexec
export PATH=$PATH:$GOPATH/bin
export PATH=$PATH:$GOROOT/bin

配置VSCode 支持Go

Command+ Shift + P
后,输入 Go:Install/Update Tools,弹出的全选安装

这里需要注意如果你使用Mac的话,可能会弹出Permission denined错误,
原因是不存在/usr/local/pkg目录,或者这个目录权限不足。执行下面命令修复:

1
2
sudo mkdir /usr/local/pkg
sudo chown -R $(whoami) /usr/local/pkg

Hello World

新建一个hello.go文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义包名。必须在源文件中非注释的第一行指明这个文件属于哪个包。
// package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
package main

// 引入fmt包
import "fmt"

// main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,
// 一般来说都是在启动后第一个执行的函数
// (如果有 init() 函数则会先执行该函数)
func main() {
fmt.Println("Hello Go!")
}
1
2
$ go run hello.go
Hello, Go!

可以使用go build hello.go来生成可执行的二进制文件。

1
2
3
4
5
$ go build hello.go
$ ls
hello hello.go
$ ./hello
Hello, go!

语法

变量

Go语言使用var identifier type的方式定义变量。
可以一次声明多个变量:

1
var identifier1, identifier2 type
1
2
3
4
5
var a string = "Geek Go"
fmt.Println(a) // 输出结果:Geek Go

var b, c int = 1, 2
fmt.Println(b, c) // 输出结果:1 2

可以使用:=声明同时赋值变量

1
2
int_val := 100        // 相当于var int_val int; int_val = 1
fmt.Println(int_val) // 输出结果:100

也可以多变量声明

1
2
3
4
var vname1, vname2, vname3 = v1, v2, v3 // 和 python 很像,不需要显示声明类型,自动推断

vname1, vname2, vname3 := v1, v2, v3 // 出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误

常量

1
const identifier [type] = value

数组

1
var variable_name [SIZE] type

例如:

1
var balance [10] float32

初始化

1
var balance = [5]int{1,2,3,4,5}

for循环

Go语言的For循环有3种形式,

1
2
3
4
5
// 等价于C语言中的for
// init: 一般为赋值表达式,给控制变量赋初值;
// condition: 关系表达式或逻辑表达式,循环控制条件;
// post: 一般为赋值表达式,给控制变量增量或减量。
for init; condition; post { }
1
2
// 等价于C语言中的while
for condition { }
1
2
// 等价于C语言中的for(;;)
for { }

栗子:

1
2
3
4
var balance = [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(balance); i++ {
fmt.Println(balance[i])
}

指针

指针使用流程:

  • 定义指针变量。
  • 为指针变量赋值。
  • 访问指针变量中指向地址的值。
1
var var_name *var_type

栗子:

1
2
var ip *int        /* 指向整型*/
var fp *float32 /* 指向浮点型 */

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

nil 指针也称为空指针。

nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

结构体

结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:

1
2
3
4
5
6
type struct_variable_type struct {
member definition
member definition
...
member definition
}

一旦定义了结构体类型,它就能用于变量的声明,语法格式如下:

1
2
3
variable_name := structure_variable_type {value1, value2...valuen}
// 或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Books struct {
title string
author string
price float32
}

func main() {
book1 := Books{"Go语言教程", "GeekHall", 9.9}
book2 := Books{title: "Python语言教程", author: "GeekHall", price: 8.8}
var book3 Books
book3.title = "西游记"
book3.author = "吴承恩"
book3.price = 19.9

fmt.Println(book1) // {Go语言教程 GeekHall 9.9}
fmt.Println(book2.title) // Python语言教程
fmt.Println(book3) // {西游记 吴承恩 19.9}
}

结构体也可以作为函数参数或者结构体指针来使用

1
2
3
4
5
6
7
8
func printBook( book *Books ) {
fmt.Printf( "Book title : %s\n", book.title)
fmt.Printf( "Book author : %s\n", book.author)
fmt.Printf( "Book price : %f\n", book.price)
}

// 调用:
printBook(&book3)

切片(Slice)

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

可以声明一个未指定大小的数组来定义切片:

1
var identifier []type

或使用 make() 函数来创建切片:

1
2
3
4
5
var slice1 []type = make([]type, len)

// 也可以简写为

slice1 := make([]type, len, [capacity])

len为数组的长度,并且也是切片的初始长度,可以使用可选参数capacity来指定容量

类似Python列表的切片操作

1
2
3
4
5
6
7
8
9
10
s := []int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := make([]int, 8, 16)
fmt.Println(s[0:4]) // [0, 1, 2, 3]
fmt.Println(s[3:]) // [3 4 5 6 7]
fmt.Println(s[:3]) // [0 1 2]
fmt.Println(s[:]) // [0 1 2 3 4 5 6 7]
fmt.Println(len(s)) // 8
fmt.Println(cap(s)) // 8
fmt.Println(len(s1)) // 8
fmt.Println(cap(s1)) // 16

可以使用appendcopy方法来拷贝和追加元素

make和new 的区别

  • make和new都是golang用来分配内存的內建函数,且在堆上分配内存,make 即分配内存,也初始化内存。new只是将内存清零,并没有初始化内存。
  • make返回的还是引用类型本身;而new返回的是指向类型的指针。
  • make只能用来分配及初始化类型为slice,map,channel的数据;new可以分配任意类型的数据。

范围(Range)

在数组上使用range将传入index和值两个变量,可使用空白符”_”省略.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nums := []int{1, 2, 3, 4}
sum := 0

for _, num := range nums {
sum += num
}
fmt.Println("sum: ", sum) // sum: 10

nums = append(nums, 5, 6, 7)
fmt.Println(nums) //[1 2 3 4 5 6 7]
for i, num := range nums {
if num%3 == 0 {
fmt.Println("index: ", i) // index : 2 和 index : 5
}
}

range也可以用在map的键值对上。

1
2
3
4
cars := map[string]string{"A": "Audi", "B": "BMW", "T": "Tesla"}
for k, v := range cars {
fmt.Printf("%s -> %s\n", k, v)
}

输出结果:

1
2
3
T -> Tesla
A -> Audi
B -> BMW

range也可以用来枚举Unicode字符串。

  • 第一个参数是字符的索引,
  • 第二个是字符(Unicode的值)本身。
1
2
3
for i, c := range "ABCabc" {
fmt.Println(i, c)
}

输出:

1
2
3
4
5
6
0 65
1 66
2 67
3 97
4 98
5 99

集合(Map)

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

定义 Map
可以使用内建函数 make 也可以使用 map 关键字来定义 Map:

1
2
3
4
5
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)

栗子:

1
2
3
4
5
6
7
8
9
10
11
var bookAuthorMap map[string]string // 创建集合
bookAuthorMap = make(map[string]string)

bookAuthorMap["三国演义"] = "罗贯中"
bookAuthorMap["西游记"] = "吴承恩"
bookAuthorMap["水浒传"] = "施耐庵"
bookAuthorMap["红楼梦"] = "曹雪芹"

for book := range bookAuthorMap {
fmt.Println(book, "作者是:", bookAuthorMap[book])
}

打印结果:

1
2
3
4
三国演义 作者是: 罗贯中
西游记 作者是: 吴承恩
水浒传 作者是: 施耐庵
红楼梦 作者是: 曹雪芹

查看元素是否在集合中存在:

1
2
3
4
5
6
jpm, ok := bookAuthorMap["金瓶梅"]
if ok {
fmt.Println("金瓶梅的作者是:", jpm)
} else {
fmt.Println("金瓶梅不是四大名著") // will print
}

delete()函数用于删除集合的元素,参数为map和其对应的key:

1
delete(bookAuthorMap, "水浒传")  // 删除后集合中只剩三本了

递归函数

1
2
3
func recursion() {
recursion() /* 函数调用自身 */
}

类型转换

1
type_name(expression)

例:

1
2
3
4
5
6
7
8
9
10
var sum int = 17
var count int = 5
var mean float32
var org int

org = sum / count
mean = float32(sum) / float32(count)

fmt.Printf("org 的值为: %d\n", org) // 3
fmt.Printf("mean 的值为: %f\n", mean) // 3.4

接口

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

type ICar interface {
drive()
}

type Tesla struct {
}

type Toyota struct {
}

type Audi struct {
}

func (tesla Tesla) drive() {
fmt.Println("I am a Tesla, I can take you for a ride.")
}

func (toyota Toyota) drive() {
fmt.Println("I am a Toyota, I can take you for a ride.")
}

func (audi Audi) drive() {
fmt.Println("I am an Audi, I can take you for a ride.")
}

func main() {
var car ICar

car = new(Tesla)
car.drive() // I am a Tesla, I can take you for a ride.

car = new(Audi)
car.drive() // I am an Audi, I can take you for a ride.

car = new(Toyota)
car.drive() // I am a Toyota, I can take you for a ride.
}

错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error类型是一个接口类型,这是它的定义:

1
2
3
type error interface {
Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

并发

Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

goroutine 语法格式:

1
go 函数名( 参数列表 )

如:

1
go f(x, y, z)

例子:

1
2
3
4
5
6
7
8
9
10
11
func concurrent_test(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

go concurrent_test("Audi")
go concurrent_test("BMW")
go concurrent_test("Tesla")
concurrent_test("Toyota")

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Audi
Toyota
BMW
Tesla
Tesla
Audi
BMW
Toyota
Audi
Toyota
BMW
Tesla
Audi
Tesla
BMW
Toyota
Toyota

通道(channel)

通道(channel)是用来传递数据的一个数据结构。

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

1
2
ch <- v    // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据,并把值赋给 v

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:

1
ch := make(chan int, [buffer])

make 的第二个参数可以指定缓冲区的大小:

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}

func main() {
s := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收

fmt.Println(x, y, x+y) // -5 17 12
}

下面的代码分配了三个缓冲区,但是同时使用4个造成了deadlock

1
2
3
4
5
6
7
8
9
c1 := make(chan int, 3)
c1 <- 100
c1 <- 200
c1 <- 300
c1 <- 400
fmt.Println(<-c1)
fmt.Println(<-c1)
fmt.Println(<-c1)
fmt.Println(<-c1) // fatal error: all goroutines are asleep - deadlock!

可以通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:

1
v, ok := <-ch

如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}

func main(){
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}

Go常用数据结构和算法

Queue

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
// initialization
queue := make([]int, 5)
for i := range queue {
queue[i] = i
}
// or
queue := make([]int, 0)
for i := 0; i < 5; i++ {
queue = append(queue, i)
}
// or
var queue []int
for i := 0; i < 5; i++ {
queue = append(queue, i)
}

// enque
queue = append(queue, 5)
fmt.Println(queue)

// deque
res := queue[0]
fmt.Println(res)
queue = queue[1:]
fmt.Println(queue)


Stack