基础语法
参考: Go Tour
1 基础
基本程序结构
每个 Go 程序都由 Package 构成. 程序总是从 main 函数开始执行. 按约定包名和导入时候的路径最后一截相同.
通过 Imports 可以引入包, 从而使用包中的公共成员. 如果一个名字以大写开头, 则为 Exported name, 即可以被包外部访问.
Go 中可以使用带名字的返回值, 这样可以在函数体中直接使用它们. 然后单个的 return 即可返回.
基本数据类型
基本数据类型有如下:
bool
string
// 根据平台自己的属性, int 可能是 64 位, 也可能是 32 位的.
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
如果变量没有赋初值, 则默认值如下:
- 数值类型初值为:
0
- 布尔类型初值为:
false
- 字符串类型初值:
""
类型转换写法:
i := 42
f := float64(i)
u := uint(f)
常量定义:
const number = 33
流程控制
循环:
// 一般的 for 循环
for i := 0; i < 10; i++ {
sum += i
}
// 上面的可以简写为如下:
for ; sum < 10; {
sum += sum
}
// while 循环的写法:
for sum < 1000 {
sum += sum
}
// 无限循环的写法:
for {
// ...
}
判断:
if x < 0 {
// ...
}
// 可以在 if 中执行一段语句后再判断:
if v := math.Pow(x, n); v < limit {
return v
}
// switch 语句和 if 类似, 也可以执行一段后再判断:
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
// 可以通过无条件的 switch 来实现简化多个 if-else 的目的:
t := time.Now()
switch {// 这里类似 switch true
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
// defer 延迟语句到 return 后执行, 调用的参数会立即计算, 但调用自己会到最后才执行
defer fmt.Println("returned")
// defer 实际是在调用栈中插入对应栈帧, 因此可以有多个 defer, 后进先出
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
/* 上述的输出为:
counting
done
9
8
7
6
5
4
3
2
1
0
*/
指针
Go 里面有指针, 指针存放值的地址:
// 创建一个指针, 默认值是 nil
var p *int
// 使用 & 可以获取值的指针
i := 42
p = &i
// 使用 * 可以解引用
*p = 21
Struct
Struct 是一系列域的集合:
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
}
Struct 的指针也是相同的 &
获取, 在指针上可以通过 .
访问成员, 不需要显式解引用.
结构体的字面值, 同时也是创建结构体的办法:
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 全部赋值, 类型为 Vertex
v2 = Vertex{X: 1} // 部分赋值, 部分默认
v3 = Vertex{} // 全部成员使用默认值
p = &Vertex{1, 2} // 全部赋值, 且 p 类型为 *Vertex
)
Array
数组固定长度的值的集合:
// 声明一个数组, 长度为 10, 里面的值都是 int 类型的, 且默认初始化为全 0
var a [10]int
// 显式的值
names := [4]string{"1", "2", "3", "4"}
// 显式赋值(通过下标)
var a [2]string
a[0] = "Hello"
a[1] = "World"
由于数组 的长度也是它类型的一部分, 因此无法扩展长度. 如果要动态长度, 则使用 slice.
Slice
切片是可变长度的, 它是一个固定长度数组的某个区域的视图:
a[low : high] // low..<high
// 切片下标是前闭后开的, 下面这个实际是 a 数组下标从 1 到 3 的值的视图
a[1:4]
切片也可以脱离数组直接存在(实际是隐式地创建一个数组并引用它):
// 普通的数组
a := [3]bool{false, true, false}
// 切片字面量: 隐式地创建数组并引用它(切片)
s := []bool{false, true, false}
如果不指定下标, 则切片引用整个数组:
var a[10]int
// 如下是 a 的切片, 它们等价:
a[:]
a[0:]
a[:10]
a[0:10]
可以获取切片的长度和容量:
var a [10]int
s := a[:]
len(s) // 长度
cap(s) // 容量
可以创建不关联任何数组的切片, 此时它的值是 nil:
var s []int
创建动态长度数组的办法是利用 make
创建切片:
a := make([]int, 5) // 长度是 5
b := make([]int, 0, 5) // 长度是 0, 容量是 5
可以使用 append
将值附加到切片, 如果原数组太小无法保存, 则会重新分配一个更长的数组并将切片指向新的数组:
s := []int{1, 2, 3}
append(s, 4)
append(s, 5, 6, 7)
同时 append 支持直接在 nil slice 上附加元素
Range
可以在 for 循环中使用 range:
array := [3]int{1, 2, 3}
// for 循环中使用 range
for index, value := range array {
// ...
}
// 可以利用 "_" 忽略 index 或 value
for _, value := range array
for index, _ := range array
Map
Map 是键值对的集合, Map 默认值是 nil, 可以通过 make
创建 Map:
// 声明一个 Map, 其默认值是 nil
var dict map[string]int
// 通过 make 创建 map
dict = make(map[string]int)
// 存放键值对
dict["hello"] = 3
Map 字面量:
// 字面量创建后仍然是可变的
dict := map[string]int{
"hello": 1,
"world": 2,
}
dict["cc"] = 999
Map 的增删改查:
// 增或者改
m[key] = value
// 删除
delete(m, key)
// 读取
val = m[key]
// 读的时候同时判断是否有这个键值对: 如果存在 exists 为 true, 且值对应读出, 否则为 false, 且值为对应类型的默认值.
value, exists := m[key]
函数变量以及闭包
go 函数可能是闭包, 即如果它引用了外部变量则就是:
// 函数作为参数:
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
// 函数闭包作为返回值(闭包指可以捕捉它上下文变量的函数)
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
2 面向对象
方法(Method)
Go 中没有类的概念, 但可以在类型上定义方法(method):
// 使用 receiver 语法定义某个类型上的方法:
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X * v.X, v.Y * v.Y)
}
// 从功能上说, 上面的方法和下面的函数等价:
func Abs(v *Vertex) float64 {
return math.Sqrt(v.X * v.X, v.Y * v.Y)
}
如果要定义不在当前包中的类型的方法, 则需要使用类型别名, 否则无法进行:
type MyFloat float64
func (f MyFloat) Abs() float64 {
// ...
}
从上面也可发现, 能在非结构体上定义 方法.
Receiver 如果是指针, 则可以改变原类型变量中的数据, 否则不能. 在调用时, go 可以自动将目标变量转换为指针进行调用. 同样地, 如果是在变量指针上调用非指针 receiver 的方法, go 也可以自动对指针进行解引用.
关于 Receiver 是否是指针, 考虑如下两个:
- 是指针, 则可以改变原来的结构内部数据, 否则不能.
- 使用指针可以避免每次都去复制传值
选择 receiver 类型的指导原则:
- 如果 receiver 是 map,func, chan,或是切片并且该方法不重新切片或重新分配, 则使用值 receiver.
- 如果该方法需要改变 receiver,则必须是指针.
- 如果包含sync.Mutex或类似的同步成员, 则必须是指针
- 如果是大型结构或数组,则指针接收器更有效
- 如果成员中也是指向可能改变的对象的指针,则更推荐指针(因为它将使读者更清楚地意图)
- 如果是一个小数组或结构,则更推荐值类型(例如,类似于 time.Time 类型),没有可变字段和没有指针,或者只是一个简单的基本类型,如 int 或 string,值接收者是有道理的,值接收器可以减少生成的垃圾量;如果将值传递给值方法,则可以使用堆栈上的副本而不是在堆上进行分配。(编译器试图避免这种分配,但它不能总是成功。)不要为此而选择值接收器类型而不先进行分析。
- 最后, 如果不满足上述, 则使用指针
接口(interface)
Go 中的接口是一系列行为的集合. 一个类型只要是实现了某接口中的所有方法, 那它就是该接口类型.
有一个要注意的地方是: 如果某类型通过指针 receiver 实现了接口中的方法, 则该类型本身没有实现该接口, 而是它的指针才实现了接口.
在 Go 中, 接口实现是隐式的, 没有任何类似 impl 的关键字, 这样的好处是将接口的实现和接口的声明完全解耦.
考虑一个接口变量 myInterface, 可以将它看成是 (value, type)
这样的结构, 即其中存放了该接口类型, 以及具体类型(concrete type).
如果像下面这样声明接口和接口对应的变量, 在调用时不会出现 nil 异常, 只是将对应具体类型设置为了 nil.
如果对为 nil 的具体类型解引用会出现空指针异常.
如果下方没有 i = t
(即接口变量中没有 value 也没有 type), 则直接调用 i.M()
也会出现空指针异常.
可以使用 fmt.Printf("(%v, %T)", i, i)
打印接口的 value 和 type.
func main() {
var i I // 接口变量
var t *T // nil 的具体类型指针
i = t // 接口变量具体类型赋值
describe(i) // 其中对 i 进行了是否是 nil 的判断
i.M() // 在具体类型为 nil 的接口
i = &T{"hello"} // 挂上新的具体类型
describe(i)
i.M()
}
使用 interface {}
可以表示任意类型, 类似 Any
, 比如:
var i interface{}
// fmt 的 printf 接收的就是一个任意类型的参数:
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
如果要对接口进行类型转换(向下), 或者判断接口是某个类型, 可以使用类似 t := i.(T)
的方式:
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
// 如果 i 是 string 类型的, 则 ok 是 true
s, ok := i.(string)
fmt.Println(s, ok)
// 这里的 ok 是 false
f, ok := i.(float64)
fmt.Println(f, ok)
// 如果没有判断, 直接进行转换且失败的话, 会直接 panic
f = i.(float64) // panic
fmt.Println(f)
如果要对接口类型进行多轮判断, 可以使用 type switch:
func do(i interface{}) {
// 判断传入参数的类型并对应操作
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
在 Go 中有一个常用的接口是 Stringer
, 它在 fmt 包中定义, 如下所示:
type Stringer interface {
String() string
}
基本上所有类型都实现了它, 可以将该类型转换为对应的字符串.
下面的例子是在自定义类型上实现 Stringer
:
type IPAddr [4]byte
func (addr IPAddr) String() string {
return fmt.Sprintf("%d.%d.%d.%d", addr[0], addr[1], addr[2], addr[3])
}
func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}
// 输出为:
// loopback: 127.0.0.1
// googleDNS: 8.8.8.8
错误(Error)
error
是内置的接口, 如下所示:
type error interface {
Error() string
}
在 Go 中的错误处理就是判断 error 是否为 nil 来确认应该进行何种操作.
自定义的错误也只需要实现 Error
方法即可用于返回 error
:
// 自定义错误类型
type MyError struct {
When time.Time
What string
}
// 注意这里是在错误类型指针上实现的 error 接口
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}
// 这里返回的是接口指针, 但接口上不需要标记为指针类型
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}
io 相关
在 Go 中定义了 Reader 接口用于表示流式 IO 的读取端, 其中有一个方法:
func (T) Read(b []byte) (n int, err error)
下面是一个简单的 Reader 使用示例, 如果 error
是 io.EOF
则停止读取:
func main() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
3 Go 泛型
泛型函数语法如下所示:
func Index[T comparable](s []T, x T) int
可以看到, 泛型参数在 []
中包裹, 其中可以包含泛型约束.
泛型类型的语法如下:
// any 表示任意类型
type List[T any] struct {
next *List[T]
val T
}
4 并发(使用 Go routine)
Go Routing 是由 Go 运行时管理的轻量级并发工具, 在使用它时, 如果利用共享内存通信, 则必须进行同步, sync
包中有许多这样的工具.
使用 go
关键字启动一个协程:
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
为了解决协程之间的同步问题, Go 提出了一个新的观点: 利用通信来共享内存, 而非共享内存来进行通信, 因此, 有了 channel 这个工具.
Channel
Channel 是有类型的管道, 用于在协程间进行通信, 使用 <-
操作符将数据传入管道, 或将管道中数据提取到变量中, 如下所示:
// 使用 make 创建管道, 其中传递的消息类型是 int 的
ch := make(chan int)
// 将 v 送入管道
ch <- v
// 从管道接收数据并把数据存入到变量
v := <-ch
// 管道作为参数的写法如下, 传入一个名为 c 的 channel 传递 int 消息
func fibonacci(n int, c chan int) {
// ...
}
默认情况下, 不管是 send 还是 receive 时, 管道的一端都会在另外一端没有准备好时处于阻塞状态, 因此通常不需要进行额外的加锁等操作.
为了减少阻塞, 可以使用带缓冲区的管道, 只有当缓冲区满的时候才会阻塞发送端, 只有缓冲区空的时候才会阻塞接收端:
// 带缓冲区的管道
ch := make(chan int, 100)
发送端可以使用 close
对管道进行关闭操作, 表示没有更多数据了:
close(ch)
接收端则可以判断管道的状态, 来决定是否继续接收数据:
// 如果 isOpen 为 false, 则表示管道关闭了
v, isOpen := <-ch
需要注意, 在被关闭了的管道上发送消息, 会触发 panic.
关闭管道都是发送端负责的, 不要在接收端关闭管道.
可以使用 select
等待多个条件, 只有它里面某个条件可用后, 才会解除阻塞:
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
select 可以有 default
块, 如果其他 case 没有, 则会直接执行 default:
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
此外, go 提供了共享资源的互斥访问 mutex 等工具.