最新版本下载地址官方下载 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.11
版本开始,Go 提供了 Go Modules 的机制,推荐设置以下环境变量,第三方包的下载将通过国内镜像,避免出现官方网址被屏蔽的问题。
1 |
$ go env -w GOPROXY=https://goproxy.cn,direct |
或在 ~/.profile
中设置环境变量
1 |
export GOPROXY=https://goproxy.cn |
go mod 是官方的包管理工具,之前有非官方的包管理工具,例如:go vendor等工具
新建一个文件 main.go
,写入
1 2 3 4 5 6 7 |
package main import "fmt" func main() { fmt.Println("Hello World!") } |
执行go run main.go
或 go 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 程序就完成了,接下来我们逐行来解读这个程序:
main
。go run main.go,其实是 2 步:
Go 语言是静态类型的,变量声明时必须明确变量的类型。Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。比如 java 中,声明一个整体一般写成 int a = 1
,在 Go 语言中,需要这么写:
1 2 3 |
var a int // 如果没有赋值,默认为0 var a int = 1 // 声明时赋值 var a = 1 // 声明时赋值 |
var a = 1
,因为 1 是 int 类型的,所以赋值时,a 自动被确定为 int 类型,所以类型名可以省略不写,这种方式还有一种更简单的表达:
1 2 |
a := 1 msg := "Hello World!" |
空值: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 |
在 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]
的值并不等于语
。str2 的长度 len(str2)
也不是 4,而是 8( Go 占 2 byte,语言占 6 byte)。正确的处理方式是将 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 |
转换成 []rune
类型后,字符串中的每个字符,无论占多少个字节都用 int32 来表示,因而可以正确处理中文。
声明数组
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...
是切片解构的写法,将切片解构为 N 个独立的元素。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 |
指针即某个值的地址,类型定义时使用符号*
,对一个已经存在的变量,使用 &
获取该变量的地址。
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 被修改 } |
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") } |
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 |
type
关键字定义了一个新的类型 Gender。 1 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 |
一个简单的累加的例子,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 |
一个典型的函数定义如下,使用关键字 func
,参数可以有多个,返回值也支持有多个。特别地,package main
中的func 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 } |
如果函数实现过程中,如果出现不能处理的错误,可以返回给调用者处理。比如我们调用标准库函数os.Open
读取文件,os.Open
有2个返回值,第一个是 *File
,第二个是 error
, 如果调用成功,error 的值是 nil,如果调用失败,例如文件不存在,我们可以通过 error 知道具体的错误信息。
1 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.New
返回自定义的错误
1 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 |
在 Python、Java 等语言中有 try...catch
机制,在 try
中捕获各种类型的异常,在 catch
中定义异常处理的行为。Go 语言也提供了类似的机制 defer
和 recover
。
1 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 |
结构体类似于其他语言中的 class,可以在结构体中定义多个字段,为结构体实现方法,实例化等。接下来我们定义一个结构体 Student,并为 Student 添加 name,age 字段,并实现 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, ...}
的形式创建 Student 的实例,字段不需要每个都赋值,没有显性赋值的变量将被赋予默认值,例如 age 将被赋予默认值 0。func
和函数名hello
之间,加上该方法对应的实例名 stu
及其类型 *Student
,可以通过实例名访问该实例的字段name
和其他方法了。实例名.方法名(参数)
的方式。除此之外,还可以使用 new
实例化:
1 2 3 4 |
func main() { stu2 := new(Student) fmt.Println(stu2.hello("Alice")) // hello Alice, I am , name 被赋予默认值"" } |
一般而言,接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口。
举一个简单的例子,定义一个接口 Person
和对应的方法 getName()
和 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
后,强制类型转换为接口类型 Person。在上面的例子中,我们在 main 函数中尝试将 Student 实例类型转换为 Person,如果 Student 没有完全实现 Person 的方法,比如我们将 (*Student).getName()
删掉,编译时会出现如下报错信息。
1 |
*Student does not implement Person (missing getName method) |
但是删除 (*Worker).getName()
程序并不会报错,因为我们并没有在 main 函数中使用。这种情况下我们如何确保某个类型实现了某个接口的所有方法呢?一般可以使用下面的方法进行检测,如果实现不完整,编译期将会报错。
1 2 |
var _ Person = (*Student)(nil) var _ Person = (*Worker)(nil) |
实例可以强制类型转换为接口,接口也可以强制类型转换为实例。
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()) } |
如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。例如
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]] } |
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!") } |
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。
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 |
假设我们希望测试 package main 下 calc.go
中的函数,要只需要新建 calc_test.go
文件,在calc_test.go
中新建测试用例即可。
1 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
,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加-v
参数。
1 2 3 4 5 |
$ go test -v === RUN TestAdd --- PASS: TestAdd (0.00s) PASS ok example 0.040s |
一般来说,一个文件夹可以作为 package,同一个 package 内部变量、类型、方法等定义可以相互看到。
比如我们新建一个文件 calc.go
, main.go
平级,分别定义 add 和 main 方法。
1 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.go
,会报错,add 未定义:
1 |
./main.go:6:14: undefined: add |
因为 go run main.go
仅编译 main.go 一个文件,所以命令需要换成
1 2 |
$ go run main.go calc.go 8 |
或
1 2 |
$ go run . 8 |
Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。
Go Modules 是 Go 1.11 版本之后引入的,Go 1.11 之前使用 $GOPATH 机制。Go Modules 可以算作是较为完善的包管理工具。同时支持代理,国内也能享受高速的第三方包镜像服务。接下来简单介绍 go mod
的使用。Go Modules 在 1.13 版本仍是可选使用的,环境变量 GO111MODULE 的值默认为 AUTO,强制使用 Go Modules 进行依赖管理,可以将 GO111MODULE 设置为 ON。
在一个空文件夹下,初始化一个 Module
1 2 |
$ go mod init example go: creating new go.mod: module example |
此时,在当前文件夹下生成了go.mod
,这个文件记录当前模块的模块名以及所有依赖包的版本。
接着,我们在当前目录下新建文件 main.go
,添加如下代码:
1 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/quote
的下载,具体的版本信息也记录在了go.mod
中:
1 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.go
中写入
1 2 3 4 5 |
package calc func Add(num1 int, num2 int) int { return num1 + num2 } |
在 package main 中如何使用 package cal 中的 Add 函数呢?import 模块名/子目录名
即可,修改后的 main 函数如下:
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 |