1.Go 安装
最新版本下载地址官方下载 golang.org
使用 Linux,可以用如下方式快速安装
1 2 3 4 5 6
$ wget https://studygolang.com/dl/golang/go1.17.7.linux-amd64.tar.gz $ tar -zxvf go1.17.7.linux-amd64.tar.gz $ sudo mv go /usr/local/ $ go version go version go1.17.7 linux/amd64
Go 1.111
$ go env -w GOPROXY=https://goproxy.cn,direct
~/.profile1
export GOPROXY=https://goproxy.cn
go mod 是官方的包管理工具,之前有非官方的包管理工具,例如:go vendor等工具
2.Hello World
main.go1 2 3 4 5 6 7
package main
import "fmt"
func main() {
	fmt.Println("Hello World!")
}
go run main.gogo run .1 2
$ go run . Hello World!
如果强制启用了 Go Modules 机制,即环境变量中设置了 GO111MODULE=on,则需要先初始化模块 go mod init hello
否则会报错误:go: cannot find main module; see ‘go help modules’
我们的第一个 Go 程序就完成了,接下来我们逐行来解读这个程序:
maingo run main.go,其实是 2 步:
- go build main.go:编译成二进制可执行程序
- ./main:执行该程序
3 变量与内置数据类型
3.1 变量(Variable)
int a = 11 2 3
var a int // 如果没有赋值,默认为0 var a int = 1 // 声明时赋值 var a = 1 // 声明时赋值
var a = 11 2
a := 1 msg := "Hello World!"
3.2 简单类型
空值:nil
整型类型: int(取决于操作系统), int8, int16, int32, int64, uint8, uint16, …
浮点数类型:float32, float64
字节类型:byte (等价于uint8)
字符串类型:string
布尔值类型:boolean,(true 或 false)
1 2 3 4 5
var a int8 = 10 var c1 byte = 'a' var b float32 = 12.2 var msg = "Hello World" ok := false
3.3 字符串
在 Go 语言中,字符串使用 UTF8 编码,UTF8 的好处在于,如果基本是英文,每个字符占 1 byte,和 ASCII 编码是一样的,非常节省空间,如果是中文,一般占3字节。包含中文的字符串的处理方式与纯 ASCII 码构成的字符串有点区别。
我们看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
package main
import (
	"fmt"
	"reflect"
)
func main() {
    str1 := "Golang"
    str2 := "Go语言"
    fmt.Println(reflect.TypeOf(str2[2]).Kind()) // uint8
    fmt.Println(str1[2], string(str1[2]))       // 108 l
    fmt.Printf("%d %c\n", str2[2], str2[2])     // 232 è
    fmt.Println("len(str2):", len(str2))       // len(str2): 8
}
str2[2]语len(str2)正确的处理方式是将 string 转为 rune 数组
1 2 3 4 5
str2 := "Go语言"
runeArr := []rune(str2)
fmt.Println(reflect.TypeOf(runeArr[2]).Kind()) // int32
fmt.Println(runeArr[2], string(runeArr[2]))    // 35821 语
fmt.Println("len(runeArr):", len(runeArr))    // len(runeArr): 4
[]rune3.4 数组(array)与切片(slice)
声明数组
1 2
var arr [5]int // 一维 var arr2 [5][5]int // 二维
声明时初始化
1 2
var arr = [5]int{1, 2, 3, 4, 5}
// 或 arr := [5]int{1, 2, 3, 4, 5}
[]1 2 3 4 5
arr := [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
	arr[i] += 100
}
fmt.Println(arr)  // [101 102 103 104 105]
数组的长度不能改变,如果想拼接2个数组,或是获取子数组,需要使用切片。切片是数组的抽象。 切片使用数组作为底层结构。切片包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展
声明切片:
1 2 3 4
slice1 := make([]float32, 0) // 长度为0的切片 slice2 := make([]float32, 3, 5) // [0 0 0] 长度为3容量为5的切片 fmt.Println(len(slice2), cap(slice2)) // 3 5
使用切片:
1 2 3 4 5 6 7 8 9
// 添加元素,切片容量可以根据需要自动扩展 slice2 = append(slice2, 1, 2, 3, 4) // [0, 0, 0, 1, 2, 3, 4] fmt.Println(len(slice2), cap(slice2)) // 7 12 // 子切片 [start, end) sub1 := slice2[3:] // [1 2 3 4] sub2 := slice2[:3] // [0 0 0] sub3 := slice2[1:4] // [0 0 1] // 合并切片 combined := append(sub1, sub2...) // [1, 2, 3, 4, 0, 0, 0]
sub2...3.5 字典(键值对,map)
map 类似于 java 的 HashMap,Python的字典(dict),是一种存储键值对(Key-Value)的数据解构。使用方式和其他语言几乎没有区别。
1 2 3 4 5 6 7 8 9
// 仅声明
m1 := make(map[string]int)
// 声明时初始化
m2 := map[string]string{
	"Sam": "Male",
	"Alice": "Female",
}
// 赋值/修改
m1["Tom"] = 18
3.6 指针(pointer)
*&1 2 3 4
str := "Golang" var p *string = &str // p 是指向 str 的指针 *p = "Hello" fmt.Println(str) // Hello 修改了 p,str 的值也发生了改变
一般来说,指针通常在函数传递参数,或者给某个类型定义新的方法时使用。Go 语言中,参数是按值传递的,如果不使用指针,函数内部将会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果参数使用指针,对参数的传递将会影响到外部变量。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
func add(num int) {
	num += 1
}
func realAdd(num *int) {
	*num += 1
}
func main() {
	num := 100
	add(num)
	fmt.Println(num)  // 100,num 没有变化
	realAdd(&num)
	fmt.Println(num)  // 101,指针传递,num 被修改
}
4 流程控制(if, for, switch)
4.1 条件语句 if else
1 2 3 4 5 6 7 8 9 10 11 12 13
age := 18
if age < 18 {
	fmt.Printf("Kid")
} else {
	fmt.Printf("Adult")
}
// 可以简写为:
if age := 18; age < 18 {
	fmt.Printf("Kid")
} else {
	fmt.Printf("Adult")
}
4.2 switch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
type Gender int8
const (
	MALE   Gender = 1
	FEMALE Gender = 2
)
gender := MALE
switch gender {
case FEMALE:
	fmt.Println("female")
case MALE:
	fmt.Println("male")
default:
	fmt.Println("unknown")
}
// male
type1 2 3 4 5 6 7 8 9 10 11 12 13
switch gender {
case FEMALE:
	fmt.Println("female")
	fallthrough
case MALE:
	fmt.Println("male")
	fallthrough
default:
	fmt.Println("unknown")
}
// 输出结果
// male
// unknown
4.3 for 循环
一个简单的累加的例子,break 和 continue 的用法与其他语言没有区别。
1 2 3 4 5 6 7
sum := 0
for i := 0; i < 10; i++ {
	if sum > 50 {
		break
	}
	sum += i
}
对数组(arr)、切片(slice)、字典(map) 使用 for range 遍历:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
nums := []int{10, 20, 30, 40}
for i, num := range nums {
	fmt.Println(i, num)
}
// 0 10
// 1 20
// 2 30
// 3 40
m2 := map[string]string{
	"Sam":   "Male",
	"Alice": "Female",
}
for key, value := range m2 {
	fmt.Println(key, value)
}
// Sam Male
// Alice Female
5 函数(functions)
5.1 参数与返回值
funcpackage mainfunc main()1 2 3
func funcName(param1 Type1, param2 Type2, ...) (return1 Type3, ...) {
    // body
}
例如,实现2个数的加法(一个返回值)和除法(多个返回值):
1 2 3 4 5 6 7 8 9 10 11 12 13
func add(num1 int, num2 int) int {
	return num1 + num2
}
func div(num1 int, num2 int) (int, int) {
	return num1 / num2, num1 % num2
}
func main() {
	quo, rem := div(100, 17)
	fmt.Println(quo, rem)     // 5 15
	fmt.Println(add(100, 17)) // 117
}
也可以给返回值命名,简化 return,例如 add 函数可以改写为
1 2 3 4
func add(num1 int, num2 int) (ans int) {
	ans = num1 + num2
	return
}
5.2 错误处理(error handling)
os.Openos.Open*Fileerror1 2 3 4 5 6 7 8 9 10 11 12 13
import (
	"fmt"
	"os"
)
func main() {
	_, err := os.Open("filename.txt")
	if err != nil {
		fmt.Println(err)
	}
}
// open filename.txt: no such file or directory
errorw.New1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import (
	"errors"
	"fmt"
)
func hello(name string) error {
	if len(name) == 0 {
		return errors.New("error: name is null")
	}
	fmt.Println("Hello,", name)
	return nil
}
func main() {
	if err := hello(""); err != nil {
		fmt.Println(err)
	}
}
// error: name is null
error 往往是能预知的错误,但是也可能出现一些不可预知的错误,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 panic。
1 2 3 4 5 6 7 8 9
func get(index int) int {
	arr := [3]int{2, 3, 4}
	return arr[index]
}
func main() {
	fmt.Println(get(5))
	fmt.Println("finished")
}
1 2 3 4
$ go run . panic: runtime error: index out of range [5] with length 3 goroutine 1 [running]: exit status 2
try...catchtrycatchdeferrecover1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
func get(index int) (ret int) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Some error happened!", r)
			ret = -1
		}
	}()
	arr := [3]int{2, 3, 4}
	return arr[index]
}
func main() {
	fmt.Println(get(5))
	fmt.Println("finished")
}
1 2 3 4
$ go run . Some error happened! runtime error: index out of range [5] with length 3 -1 finished
- 在 get 函数中,使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer。
- 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0。
6 结构体,方法和接口
6.1 结构体(struct) 和方法(methods)
hello()1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
type Student struct {
	name string
	age  int
}
func (stu *Student) hello(person string) string {
	return fmt.Sprintf("hello %s, I am %s", person, stu.name)
}
func main() {
	stu := &Student{
		name: "Tom",
	}
	msg := stu.hello("Jack")
	fmt.Println(msg) // hello Jack, I am Tom
}
Student{field: value, ...}funchellostu*Studentname实例名.方法名(参数)new1 2 3 4
func main() {
	stu2 := new(Student)
	fmt.Println(stu2.hello("Alice")) // hello Alice, I am  , name 被赋予默认值""
}
6.2 接口(interfaces)
一般而言,接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口。
PersongetName()getAge()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
type Person interface {
	getName() string
}
type Student struct {
	name string
	age  int
}
func (stu *Student) getName() string {
	return stu.name
}
type Worker struct {
	name   string
	gender string
}
func (w *Worker) getName() string {
	return w.name
}
func main() {
	var p Person = &Student{
		name: "Tom",
		age:  18,
	}
	fmt.Println(p.getName()) // Tom
}
Student(*Student).getName()1
*Student does not implement Person (missing getName method)
(*Worker).getName()1 2
var _ Person = (*Student)(nil) var _ Person = (*Worker)(nil)
- 将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法。
- Worker 同上。
实例可以强制类型转换为接口,接口也可以强制类型转换为实例。
1 2 3 4 5 6 7 8 9
func main() {
	var p Person = &Student{
		name: "Tom",
		age:  18,
	}
	stu := p.(*Student) // 接口转为实例
	fmt.Println(stu.getAge())
}
6.3 空接口
如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。例如
1 2 3 4 5 6 7
func main() {
	m := make(map[string]interface{})
	m["name"] = "Tom"
	m["age"] = 18
	m["scores"] = [3]int{98, 99, 85}
	fmt.Println(m) // map[age:18 name:Tom scores:[98 99 85]]
}
7 并发编程(goroutine)
7.1 sync
Go 语言提供了 sync 和 channel 两种方式支持协程(goroutine)的并发。
例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import (
	"fmt"
	"sync"
	"time"
)
var wg sync.WaitGroup
func download(url string) {
	fmt.Println("start to download", url)
	time.Sleep(time.Second) // 模拟耗时操作
	wg.Done()
}
func main() {
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go download("a.com/" + string(i+'0'))
	}
	wg.Wait()
	fmt.Println("Done!")
}
- wg.Add(1):为 wg 添加一个计数,wg.Done(),减去一个计数。
- go download():启动新的协程并发执行 download 函数。
- wg.Wait():等待所有的协程执行结束。
1 2 3 4 5 6 7
$ time go run . start to download a.com/2 start to download a.com/0 start to download a.com/1 Done! real 0m1.563s
可以看到串行需要 3s 的下载操作,并发后,只需要 1s。
7.2 channel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
var ch = make(chan string, 10) // 创建大小为 10 的缓冲信道
func download(url string) {
	fmt.Println("start to download", url)
	time.Sleep(time.Second)
	ch <- url // 将 url 发送给信道
}
func main() {
	for i := 0; i < 3; i++ {
		go download("a.com/" + string(i+'0'))
	}
	for i := 0; i < 3; i++ {
		msg := <-ch // 等待信道返回消息。
		fmt.Println("finish", msg)
	}
	fmt.Println("Done!")
}
使用 channel 信道,可以在协程之间传递消息。阻塞等待并发协程返回消息。
1 2 3 4 5 6 7 8 9 10
$ time go run . start to download a.com/2 start to download a.com/0 start to download a.com/1 finish a.com/2 finish a.com/1 finish a.com/0 Done! real 0m1.528s
8 单元测试(unit test)
calc.gocalc_test.gocalc_test.go1 2 3 4 5 6
// calc.go
package main
func add(num1 int, num2 int) int {
	return num1 + num2
}
1 2 3 4 5 6 7 8 9 10
// calc_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
	if ans := add(1, 2); ans != 3 {
		t.Error("add(1, 2) should be equal to 3")
	}
}
go test-v1 2 3 4 5
$ go test -v === RUN TestAdd --- PASS: TestAdd (0.00s) PASS ok example 0.040s
9 包(Package)和模块(Modules)
9.1 Package
一般来说,一个文件夹可以作为 package,同一个 package 内部变量、类型、方法等定义可以相互看到。
calc.gomain.go1 2 3 4 5 6
// calc.go
package main
func add(num1 int, num2 int) int {
	return num1 + num2
}
1 2 3 4 5 6 7 8
// main.go
package main
import "fmt"
func main() {
	fmt.Println(add(3, 5)) // 8
}
go run main.go1
./main.go:6:14: undefined: add
go run main.go1 2
$ go run main.go calc.go 8
或
1 2
$ go run . 8
Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。
9.2 Modules
go mod在一个空文件夹下,初始化一个 Module
1 2
$ go mod init example go: creating new go.mod: module example
go.modmain.go1 2 3 4 5 6 7 8 9 10 11
package main
import (
	"fmt"
	"rsc.io/quote"
)
func main() {
	fmt.Println(quote.Hello())  // Ahoy, world!
}
go run .rsc.io/quotego.mod1 2 3 4 5
module example go 1.13 require rsc.io/quote v3.1.0+incompatible
我们在当前目录,添加一个子 package calc,代码目录如下:
1 2 3 4
demo/
   |--calc/
      |--calc.go
   |--main.go
calc.go1 2 3 4 5
package calc
func Add(num1 int, num2 int) int {
	return num1 + num2
}
import 模块名/子目录名1 2 3 4 5 6 7 8 9 10 11 12 13
package main
import (
	"fmt"
	"example/calc"
	"rsc.io/quote"
)
func main() {
	fmt.Println(quote.Hello())
	fmt.Println(calc.Add(10, 3))
}
1 2 3
$ go run . Ahoy, world! 13
