30 分钟极速上手 Go
Go 开发环境
安装 Go (mac)
> brew install go
> go env GOROOT
> export GOPATH=$HOME/workspace/go
> mkdir -p $GOPATH/{src,bin}
设置好环境变量 GOROOT
和 GOPATH
:
# .zshrc or .bashrc
# Go
export GOPATH=$HOME/workspace/go
export GOROOT=/usr/local/opt/go/libexec
GOROOT
乃 Go 安装的本地目录
Go 依照“everything under one roof”原则,GOPATH
乃 Go 工作区(workspace),可以是一个或多个目录(通常建议是一个),存放项目有关的一切:源代码,第三方库,编译中间文件,等等,GOPATH 目录下,约定有三个子目录:
- src:存放源代码
- pkg:存放编译时,生成的中间文件
- bin:存放编译后生成的可执行文件 (通常会将 $GOPATH\bin 加入环境变量 PATH 中,以方便执行编译后的程序)
设置方式:
- 一个工作区,全局多个项目共享,简单,但隔离不够
- 每个项目使用单独工作区,每次要单独指定 GOPATH
- 项目/业务和第三方库分开放置,配置两个 GOPATH
GOBIN
是编译之后,可执行文件的安装目录。如果设置了 GOBIN,编译后的可执行文件将不会安装到 GOPATH 下的 bin 目录。如果 GOPATH 包含了多个目录,则必须设置 GOBIN。
💡 GOPATH 简单,但难以处理版本依赖和隔离,Go1.14 推荐在生产上使用 Go Module(类似 NPM,Go 语言的依赖解决方案),取代 GOPATH
VS Code
安装 Go 插件:
Goland
设置好 GOROOT 和 GOPATH 即可。
Go 项目工程结构
在编码开发之前,我们看下 go 的项目结构。
基于 Go Module,可以在任意位置创建一个 Go 项目,而不再像以前一样局限在$GOPATH/src 目录下。假设要创建一个 hello 项目,它位于~/workspace/go 目录下,输入如下命令即可创建一个 Go Module 工程:
~/workspace/go via 🐹 v1.15.5
[I] ➜ mkdir hello
~/workspace/go via 🐹 v1.15.5
[I] ➜ go mod init fastzhong.com/hello
go: creating new go.mod: module fastzhong.com/hello
~/workspace/go via 🐹 v1.15.5
[I] ➜ ls
go.mod pkg
~/workspace/go via 🐹 v1.15.5
[I] ➜ cat go.mod
───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: go.mod
───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │ module fastzhong.com/hello
2 │
3 │ go 1.15
───────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
当前生成的 Go Module 工程只有一个 go.mod 文件,go.mod 文件是 Go 语言工具链用于管理 Go 语言项目的一个配置文件,不用手动修改它,Go 语言的工具链会帮我们自动更新,比如当我们的项目添加一个新的第三方库/包的时候,比如:
import (
"github.com/gohugoio/hugo/commands"
)
以上引入的 github.com/gohugoio/hugo/commands 这个包是属于 github.com/gohugoio/hugo/这个 Go Module 的。
相应的可以在我们自己的 Go Module 工程里创建一些包(其实就是子目录),比如 lib1、lib2,那么它的对应的包就是 fastzhong.com/hello/lib1,其他包只有通过这个包名才能使用 lib1 包中的函数方法等。
.
├── go.mod
├── lib1
├── lib2
├── pkg
└── main.go
所以每个子目录都是一个包,子目录里可以放 go 源文件。
第三方包的获取可以通过go get:
Hello World
在~/workspace/go/hello 下添加一个 main.go 文件:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World!")
}
//运行main
workspace/go/hello via 🐹 v1.15.5 took 6s
[I] ➜ go run main.go
Hello World!
// 打包安装
workspace/go/hello via 🐹 v1.15.5
[I] ➜ go install fastzhong.com/hello
// 直接运行可执行二进制
workspace/go/hello via 🐹 v1.15.5
[I] ➜ hello
Hello World!
程序结构
从上面的 hello.go 可以看出:
✦ 源文件以 .go 为后缀名存储
✦ package(包)
Go 使用 package(包)来管理代码,package 特性:
- 一个目录下的同级文件属于一个 package
- 包名与目录名可以不同,但是通常会保持一致
- package 是一个或多个 Go 源码文件的集合(内容包含了变量,struct,类型,接口,函数),Go 的包更像 Python,而非 Java(内容以 class 为基本单位,万事皆 class),每个程序可以直接使用自身的包或者引用其它的包:
- 声明:在源文件的第一行声明明该文件属于哪一个包,如:package myservice,注意包名没有层级概念
- 引用:需要加入引用路径(目录名),如 “import xxx.com/a/b/myservice“ 就对应着 src 目录下的 xxx.com/a/b/myservice,这点和 Java 不同,Go 的声明和引用路径是分开的
- 程序的入口是 main 包的 main 函数,如果没有 main 包,则不会生成可执行文件,main 包不可被引用
- Go 语言也有类似 Java Public 和 Private 的概念,如果类型/接口/方法/函数/字段的首字母大写(例如 fmt.Print),则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见
✦ Go 编译生成的是一个静态可执行文件,除了 glibc 外没有其他外部依赖
数据结构
✦ 基础类型
布尔类型:bool
整型:byte、int、int8、int16、uint、uintptr
浮点类型:float32、float64
复数类型:complex64、complex128
字符串:string
字符类型:rune
错误类型:error
error Go 内建错误类型,其实是一个接口类型(interface)
✦ 复合类型
指针:pointer
数组:array
切片:slice
字典:map
通道:chan
结构体:struct
接口:interface
💡struct & pointer 和 C/C++很类似 😅,理解指针的关键点是内存模型(变量/数据在内存中的样子)。
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 被修改
}
slice 相当于动态数组,其用法和 Python 类似:
a := [...]int{1, 2, 3, 4, 5}
s1 := a[2:4] // 3,4
s2 := a[1:5] // 2,3,4,5
s3 := a[:] // a
s4 := a[:4] // 1,2,3,4
s5 := s2[:] // 1,2,3,4,5
s6 := a[2:4:5] // 3
逻辑结构
✦ if/els & switch
if x > 0 {
return "positive"
} else {
return "negative"
}
switch t := i.(type) {
case bool:
return "I'm a bool"
case int:
return "I'm an int"
default:
return "Don't know type %T", t
}
✦ for
- for init; condition; post { } // 类似 C
- for condition { } // 相当于 while
- for { } // 无限循环
for i := 0; i < 10; i++ {
sum += i
}
循环体内可以有 break 和 continue
✦ goto
if skipped {
goto doLast
}
// 跳过上面的代码块
doLast:
// 立即执行的代码块
✦ range
和 Python 类似:
list := []string{"a", "b", "c", "d", "e", "f"}
for i, v := range list {
// i is the element index
// v is the element value
}
内置函数
✦ main() & init()
和 Python 类似,init 的作用在于初始化:
- init 函数先于 main 函数自动执行,不能被其他函数调用;
- init 函数没有输入参数、返回值;
- 每个包可以有多个 init 函数,每个源文件也可以有多个 init 函数;
- 同一个包的 init 执行顺序,golang 没有明确定义,编程时要注意程序不要依赖这个执行顺序;
- 不同包的 init 函数按照包导入的依赖关系决定执行顺序;
✦ defer
defer 修饰的代码,不会马上执行,而是在当前函数返回或退出之前执行,常用于关闭/释放资源,如文件描述符,数据库链接,锁等:
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
$ go run main.go
4
3
2
1
✦ panic() & recover()
panic 抛出异常,recover 用来俘获,并抛向上一层:
func action() {
defer fmt.Println("action completed")
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
// do something to recover
}
}()
panic("something goes wrong !!!")
}
// something goes wrong !!!
// action completed
✦ new() & make()
- make 和 new 都是 golang 用来分配内存的內建函数,且在堆上分配内存,make 即分配内存,也初始化内存。new 只是将内存清零,并没有初始化内存。
- make 返回的还是引用类型本身;而 new 返回的是指向类型的指针。
- make 只能用来分配及初始化类型为 slice,map,channel 的数据;new 可以分配任意类型的数据。
还有其它的: close,delete,len,cap,copy,append,print,println,complex,real,imag
变量
// 单个变量
var i int = 0
var i = 0
i := 0
// 多个变量
var i, j, k int
// 常量:
const (
a = iota // a=0
b // 和 a 一样,不用重复 b = iota,iota会自增,b=1
c // c=2
d = 1
f // 和 d 一样,f=1
)
函数
在 Go 语言中函数是一等公民,它作为一个变量、类型、参数、返回值,甚至可以去实现一个接口,但是 Go 语言中函数不支持重载、嵌套和默认参数。
✦ 函数定义:
func function_name( [parameter list] ) [return_types] {
// 函数体
}
✦ 不定长度变参:
func test(num ...int){
fmt.Println(num) // [1 2 3 4]
}
test(1,2,3,4)
✦ 多返回值:
func test() (string,int,bool){
return "Hello World", 100, true
}
v1, v2, v3 := test()
fmt.Println(v1, v2, v3) // Hello World 100 true
✦ 命名返回值:
func test() (a string, b bool, c int) {
a = "Golang"
b = false
c = 200
return
}
v1, v2, v3 := test()
fmt.Println(v1, v2, v3) // Golang false 200
✦ 作为变量:
func test(){
// 函数体
}
funcTest := test
fmt.Println(funcTest())
✦ 匿名函数:
test := func(){
// 函数体
}
✦ 作为类型:
package main
import "fmt"
type iAdder func(int, int) int
func main(){
var adder iAdder = func(a int, b int) int {
return a + b
}
fmt.Println(adder(1,2)) // 3
}
✦ 闭包:
package main
import "fmt"
// 使用 闭包实现 斐波那契数列
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a +b
return a
}
}
func main() {
f := fibonacci()
fmt.Println(f()) // 1
fmt.Println(f()) // 1
fmt.Println(f()) // 2
fmt.Println(f()) // 3
fmt.Println(f()) // 5
}
💡 理解闭包的关键点是作用域
结构体(struct) 和方法(methods)
与 C 语言中的 struct 或其他面向对象编程语言中的 class 类似,
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
}
接口(interfaces)
和 Java 类似,接口避免多继承(multi-inheritance),而采用组合方式(composition)。接口定义了一组抽象行为 - 方法(method):
type Person interface {
getName() string
getAge() int
}
type Student struct {
name string
age int
}
func (stu *Student) getName() string {
return stu.name
}
func (stu *Student) getAge() int {
return stu.age
}
func main() {
var p Person = &Student{
name: "Tom",
age: 18,
}
fmt.Println(p.getName()) // Tom
}
- 接口不能被实例化,一个类型可以实现多个接口
- 不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可
- 方法和函数不同,不可直接调用,必须通过实现接口的实例调用
小结:如果有 Java,Javascript,C++,Python 编程经验,上手 Go 确实很快,Go 借鉴了它们的特点。
㊫ Why Go or Go Why
- The Why of Go (Carmen Andoh)
- Simplicity is Complicated(Rob Pike)
- Go: building on the shoulders of giants and stepping on a few toes(Steve Francia)
- The Go Language: What Makes it Different? (Jay McGavren)
㊫ 更深入的教程