Go 学习笔记
Go 学习笔记
目录:
- 简介
- Go 程序的基本结构
- 流程控制语句
- 更多类型:结构体、数组、切片、映射
- 方法和接口
- 并发机制
一、简介
Go 语言之旅:https://tour.go-zh.org/list
Go 是从 2007 年末由 Robert Griesemer, Rob Pike, Ken Thompson 主持开发,后来还加入了 Ian Lance Taylor, Russ Cox 等人,并最终于 2009 年 11 月开源,在 2012 年早些时候发布了 Go 1 稳定版本。现在 Go 的开发已经是完全开放的,并且拥有一个活跃的社区。
Go 语言特性:并发、静态类型的编译性语言、垃圾回收的便利性、运行时反射快速,有点像动态类型的解释语言(JS)。
Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。
对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。
二、Go 程序的基本结构
1. 包
每个 Go 程序都是由包构成的。程序从 main 包开始运行。
通过 import 语句来导入包。
**注意:**在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。未以大写字母开头的是未导出的。任何“未导出”的名字在该包外均无法访问。
2. 函数
函数通过 func 关键字来声明。函数可以没有参数或接受多个参数。参数需要有类型声明。
当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。
函数可以返回任意数量的返回值。
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
3. 变量
var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。
变量声明可以包含初始值,每个变量对应一个。
如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。
package main
import "fmt"
var i, j int = 1, 2
func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
// output:1 2 true false no!
短变量声明:
在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。
函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。
package main
import "fmt"
func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
fmt.Println(i, j, k, c, python, java)
}
// output: 1 2 3 true false no!
4. 基本类型
Go 的基本类型有:
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名
// 表示一个 Unicode 码点
float32 float64
complex64 complex128
零值:
没有明确初始值的变量声明会被赋予它们的 零值。
零值是:
- 数值类型为
0, - 布尔类型为
false, - 字符串为
""(空字符串)。
类型转换:
表达式 T(v) 将值 v 转换为类型 T。
Go 在不同类型的项之间赋值时需要显式转换。
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
类型推导:
在声明一个变量而不指定其类型时(即使用不带类型的 := 语法或 var = 表达式语法),变量的类型由右值推导得出。
当右值声明了类型时,新变量的类型与其相同;当右边包含未指明类型的数值常量时,新变量的类型就可能是 int, float64 或 complex128 了,这取决于常量的精度。
5. 常量
常量的声明与变量类似,只不过是使用 const 关键字。
常量可以是字符、字符串、布尔值或数值。
常量不能用 := 语法声明。
三、流程控制语句
1. for
Go 只有一种循环结构:for 循环。
基本的 for 循环由三部分组成,它们用分号隔开:
- 初始化语句:在第一次迭代前执行
- 条件表达式:在每次迭代前求值
- 后置语句:在每次迭代的结尾执行
初始化语句通常为一句短变量声明,该变量声明仅在 for 语句的作用域中可见。
一旦条件表达式的布尔值为 false,循环迭代就会终止。
初始化语句和后置语句是可选的。
for 是 Go 中的 “while”。
注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。
package main
import "fmt"
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}
//无限循环。如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。
for {
}
2. if
Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。
if 的简短语句:
同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内。
在 if 的简短语句中声明的变量同样可以在任何对应的 else 块中使用。
package main
import (
"fmt"
"math"
)
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
} else {
fmt.Printf("%g >= %g\n", v, lim)
}
// 这里开始就不能使用 v 了
return lim
}
func main() {
fmt.Println(
pow(3, 2, 10),
pow(3, 3, 20),
)
}
3. switch
Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。
switch 的 case 语句从上到下顺次执行,直到匹配成功时停止。
4. defer
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
package main
import "fmt"
func main() {
fmt.Println("counting")
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
// output:
counting
done
2
1
0
四、更多类型:结构体、数组、切片、映射
1. 指针
Go 拥有指针。指针保存了值的内存地址。
类型 *T 是指向 T 类型值的指针。其零值为 nil。
& 操作符会生成一个指向其操作数的指针。
* 操作符表示指针指向的底层值。
与 C 不同,Go 没有指针运算。
package main
import "fmt"
func main() {
i, j := 42, 2701
p := &i // 指向 i
fmt.Println(*p) // 通过指针读取 i 的值: 42
*p = 21 // 通过指针设置 i 的值
fmt.Println(i) // 查看 i 的值: 21
p = &j // 指向 j
*p = *p / 37 // 通过指针对 j 进行除法运算
fmt.Println(j) // 查看 j 的值: 73
}
// output:
42
21
73
2. 结构体
一个结构体(struct)就是一组字段(field)。
结构体字段使用点号来访问。
**结构体指针:**结构体字段可以通过结构体指针来访问。如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。
结构体文法:
结构体文法通过直接列出字段的值来新分配一个结构体。
使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)
特殊的前缀 & 返回一个指向结构体的指针。
package main
import "fmt"
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)
func main() {
fmt.Println(v1, p, v2, v3)
}
// output: {1 2} &{1 2} {1 0} {0 0}
3. 数组
类型 [n]T 表示拥有 n 个 T 类型的值的数组。
数组的长度是其类型的一部分,因此数组不能改变大小。这看起来是个限制,不过没关系,Go 提供了更加便利的方式来使用数组。
4. 切片
每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。
类型 []T 表示一个元素类型为 T 的切片。
package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
// output:
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]
切片就像数组的引用:
切片并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
与它共享底层数组的切片都会观测到这些修改。
切片的默认行为:切片下界的默认值为 0,上界则是该切片的长度。
切片拥有 长度 和 容量,可通过表达式 len(s) 和 cap(s) 来获取。
切片的零值是 nil。nil 切片的长度和容量为 0 且没有底层数组。
切片操作:
为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append 函数。
append 的结果是一个包含原切片所有元素加上新添加元素的切片。
当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。
5. Range
for 循环的 range 形式可遍历切片或映射。
package main
import "fmt"
var pow = []int{1, 2, 4}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
// output:
2**0 = 1
2**1 = 2
2**2 = 4
6. 映射
映射将键映射到值。
映射的零值为 nil 。nil 映射既没有键,也不能添加键。
make 函数会返回给定类型的映射,并将其初始化备用。
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
func main() {
fmt.Println(m) // map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
m["Answer"] = 42 // 插入或修改元素
fmt.Println("The value:", m["Answer"]) // 42
delete(m, "Answer") // 删除元素
fmt.Println("The value:", m["Answer"]) // 0
v, ok := m["Answer"] // 通过双赋值检测某个键是否存在:
fmt.Println("The value:", v, "Present?", ok) // The value: 0 Present? false
}
五、方法和接口
1. 方法
Go 没有类。不过你可以为结构体类型定义方法。
方法就是一类带特殊的 接收者 参数的函数。
方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
// Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs()) // 5
}
方法只是个带接收者参数的函数。
也可以为非结构体类型声明方法。
可以为指针接收者声明方法。
2. 接口
接口类型 是由一组方法签名定义的集合。可以理解为类的概念。
接口类型的变量可以保存任何实现了这些方法的值。
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
package main
import (
"fmt"
)
// 我们定义了一个接口Phone,接口里面有一个方法call()
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone // 定义了一个Phone类型变量
phone = new(NokiaPhone) // 为变量phone赋值NokiaPhone
phone.call() // I am Nokia, I can call you!
phone = new(IPhone) // 为变量phone赋值IPhone
phone.call() // I am iPhone, I can call you!
}
类型断言与类型选择:
package main
import "fmt"
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)
}
}
func main() {
do(21) // Twice 21 is 42
do("hello") // "hello" is 5 bytes long
do(true) // I don't know about type bool!
}
六、并发机制
1. Go 程(Goroutines)
Go 程(goroutine)是由 Go 运行时管理的轻量级线程。go f(x, y, z) 会启动一个新的 Go 程并执行 f(x, y, z),f, x, y 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。
Goroutines 具有自己的调用堆栈,该调用堆栈会根据需要进行扩展和收缩。
2. 信道
信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。
信道非常适合在各个 Go 程间进行通信。
ch := make(chan int) // 和映射与切片一样,信道在使用前必须创建
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
// “箭头”就是数据流的方向。
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- 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)
}
// output: -5 17 12
互斥锁:
但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?
这里涉及的概念叫做 互斥(mutualexclusion)* ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。
Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:Luck 和 Unlock。
select 语句:
select 语句使一个 Go 程可以等待多个通信操作。
select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。