Go 学习笔记
基本语法
注释
/*
* 行间注释
*/
// 行内注释
变量
var a int // 定义一个整型变量
var b, c bool = true, false // 定义两个布尔变量并初始化
// 变量声明的分组样式,factored style
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = complx.Sqrt(-5 + 12i)
)
func main() {
k := 3 // 短声明,函数内可以使用
}
注意,
:=
是声明符,不是赋值符
数组和切片
var a [10]int // 长度为 10 的整型数组,其类型为 [10]int
a[0] = 1 // 数组元素赋值
primes := [6]int{2, 3, 5, 7, 11, 13} // 数组字面量
var s []int = primes[1:4] // 切片类似 Python 的数组引用,前闭后开。类型为 []T
s := []int{2, 3} // 切片字面量
s = append(s, 5) // 向切片添加元素
a := make([]int, 5) // 可以使用 make 构造切片
map
var m map[string]int // 声明一个 string: int 的 map
m = make(map[string]int) // 可以用 make 创建 map 实例
// 也可以定义 map 字面量
var m = map[string]int{
"math_score": 150,
"phy_score": 110,
}
// 如果顶级类型仅是一个类型名称,则可以从字面量的元素中省略它
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
// map 操作
m[key] // 插入/取出/更新元素
delete(m, key) // 删除元素
elem, ok = m[key] // 检测 key 是否在 map 中。如果在,则 ok == true
elem, ok := m[key] // 如果 elem, ok 还没有被声明,你可以使用这种短声明形式
函数
// 一个基本函数
func add(x, y int) int {
return x + y
}
// 返回多个值的函数
func swap(x, y string) (string, string) {
return y, x
}
a, b := swap("world", "hello") // 接收函数返回值
函数闭包
函数闭包是一个捕获了外部变量的函数。
func adder() func(int) int { // 声明一个返回值为函数的函数
sum := 0 // 属于 adder() 的变量
return func(x int) int {
sum += x // 捕获了属于 adder() 的 sum 变量
return sum
}
}
每次调用 adder()
时都返回一个闭包,这个闭包和本次调用的 adder()
内的 sum
绑定
指针
var p *int // p 是一个指向 int 的指针
p = &a // 取地址
*p = 2 // 解引用
分支逻辑
循环
Go 只有 for
循环,没有 while
循环。
// 一个基本的 for 循环
for i := 0; i < 10; i++ {
sum += i
}
// 范围循环
for i, v := range arr {
fmt.Printf("index: %d, value: v\n", i, v)
}
// 可以忽略 v 值
for i := range arr {
fmt.Printf("index: %d\n", i)
}
// 也可以忽略 i 值,但要用 _ 指出
for _, v := range arr {
fmt.Printf("value: %d\n", v)
}
// for 其实就是 go 语言的 while
for sum < 1000 {
sum += sum
}
// 无限循环
for {
sum += 1
}
if 和 switch
if x < 0 {
sum += 1
} else {
sum -= 1
}
// if 可以带一个短表达式
if v := math.Pow(x, n); v < lim { // 只有首字母大写的名字会被导出,因此是 Pow
return v
}
// switch,不需要 break
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)
}
结构体和方法
结构体
Go 的结构体相当于 C++ 中的类
type Vertex struct {
X int // 在 Go 中,首字母大写的变量/函数等是 public 的,首字母小写的是 private 的
Y int
}
func main() {
v := Vertex{1, 2} // 定义一个结构体字面量
v.X = 4 // 使用结构体
p := &v // p 是结构体的指针
p.X = 1e9 // Go 可以隐式解引用
v2 := Vertex{X: 1} // 可以只为指定成员变量赋值
}
type
关键字的特殊用法:你可以将基本数据类型包装为自定义类型。
type MyFloat float64 // 定义 MyFloat 为 float64
type IPAddr [4]byte // 定义 IP 地址类型为 4 字节的数组
方法
Go 语言没有类,但是你可以为类型指定方法。
方法,相当于 C++ 中类的成员函数
// 通过在函数名前面加上 receiver 参数列表来将函数变为方法
func (v Vertex) Abs() float64 { // (v Vertex) 表示该方法的操作对象是类型为 Vertex 的变量 v
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// 由于方法经常用来改变 receiver 内部的值,所以 receiver 指针更为常用
func (v *Vertex) Scale(f float64) {
v.X = v.X * f // 改变了 v 内部的值
v.Y = v.Y * f
}
var v Vertex // 声明一个 Vertex 变量
v.Scale // Scale() 方法的操作对象本来应该是指向 Vertex 的指针,但是 go 进行了指针间接访问,把 v.Scale 解释为了 (&v).Scale
p = &v // 声明一个指向 Vertex 的指针
p.Abs() // 反过来也是,Abs() 方法的操作对象本来应该是 Vertex 变量,但是却提供了指针。go 将 p.Abs 解释为了 (*p).Abs
接口
接口是方法签名的集合
type Abser interface {
Abs() float64 // 接口方法
}
// 任何实现了接口方法的类型都隐式实现了接口
func (f MyFloat) Abs() float64 { // MyFloat 实现了 Abser 接口,那么 MyFloat 就是一种 Abser
...
}
func (v *Vertex) Abs() float64 { // Vertex 实现了 Abser 接口
// 可以这样优雅地处理“空指针”引用
if t == nil {
fmt.Println("<nil>")
return
}
// 接口变量可以保存任何实现了接口方法的结构体的实例
var a Abser // nil,一个零值接口既不持有值也不持有具体类型
a = MyFloat(-math.Sqrt(2)) // 可以赋值为 MyFloat
a = &Vertex{3, 4} // 也可以赋值为 Vertex 指针
// 空接口 interface{} 被用于处理未知类型值的代码。例如,fmt.Print 接受任意数量的 interface{} 类型参数
// 在 Go 1.18 中,interface{} 被 any 类型代替
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
// 等价于
func describe(i any) {
fmt.Printf("(%v, %T)\n", i, i)
}
// 类型断言
f, ok := i.(float64) // 如果接口 i 的底层是 float64,那么 f = float64,否则 ok = false
// 类型 switch
switch v := i.(type) {
case T:
// 在这里 v 的类型为 T
case S:
// 在这里 v 的类型为 S
default:
// 都不匹配的情况; 在这里 v 的类型和 i 相同
}
// 最常见的接口:fmt 包定义的 Stringer 接口
type Stringer interface {
String() string
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
错误
Go 程序用 error
值表示错误状态。
error
类型是一个内置接口,类似于 fmt.Stringer
:
type error interface {
Error() string
}
函数通常会返回一个 error
值,调用代码应通过测试 error
是否等于 nil
来处理错误。
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
// 实现一个 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)
}
协程
Go 协程(goroutines)相当于 C++ 中的线程。
func foo() { // 协程函数
...
}
go foo() // 启动协程
通道
通道是一种类型化的管道,通过它你可以使用通道操作符 <-
发送和接收值。
默认情况下,发送和接收操作会阻塞,直到另一方准备就绪。这使得 goroutine 可以在没有显式锁或条件变量的情况下进行同步。
ch := make(chan int) // 可以使用 map 创建通道
ch <- v // 将 v 发送到通道 ch
v := <-ch // 从通道 ch 接收,并赋值给 v
// 声明接收通道参数的函数
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 将 sum 发送给通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
sum(s[:len(s)/2], c)
x := <-c // 从通道 c 接收。main 函数在这里等待 sum 函数返回
fmt.Println(x)
}
带有缓冲区的通道:
ch := make(chan int, 100) // 创建一个缓冲区长度为 100 的通道
当缓冲区已满时,发送操作会被阻塞。当缓冲区为空时,接收操作会被阻塞。
关闭通道:
close(ch) // 发送方可以关闭通道以指示不会再发送更多值
v, ok := <-ch // 接收方可以接收第二个参数来测试通道是否已关闭
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x // 发送一个计算结果
x, y = y, x+y
}
close(c) // 关闭通道以表示计算结束
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// 循环会重复地从通道接收值,直到通道被关闭。
for i := range c {
fmt.Println(i)
}
}
select 语句允许一个 goroutine 等待多个通信操作。
一个 select
阻塞,直到其中一个 case
可以运行,然后执行该 case
。如果有多个 case
准备就绪,则随机选择一个。
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: // 向 c 发送
x, y = y, x+y
case <-quit: // 从 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) // 从 c 取数据
}
quit <- 0 // 停止阻塞 quit
}()
fibonacci(c, quit)
}
如果没有其他 case
准备就绪,那么运行 default
。
select {
case i := <-c:
// use i
default:
// receiving from c would block
}
互斥锁
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter 可以安全地并发使用。
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc 对给定 key 的计数器加 1
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// 获取锁,以便一次只有一个 goroutine 可以访问 map c.v
c.v[key]++
c.mu.Unlock()
}
// Value 返回给定 key 的计数器的当前值
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// 获取锁,以便一次只有一个 goroutine 可以访问 map c.v
defer c.mu.Unlock() // 使用 defer 确保在 return 之后再释放锁
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)} // 这里只对 map v 赋值
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
同步锁
可以使用 sync.WaitGroup
来管理并等待所有的协程完成。
var wg sync.WaitGroup // 创建一个同步锁
for _, u := range urls {
wg.Add(1) // 同步锁信号量 +1
go Crawl(u, depth-1, fetcher, c, &wg) // 启动一个协程
}
subWg.Wait() // 等待所有协程结束
defer 关键字
一个 defer
语句会延迟函数的执行,直到其所在的函数返回。
func main() {
defer fmt.Println("world") // 在 main 函数 return 之前才执行
fmt.Println("hello")
}
包管理
package main
// 导入单个包
import "fmt"
// 导入多个包
import (
"fmt"
"math"
)
import f "fmt" // 给导入的包 fmt 起别名 f
import _ "fmt" // 导入包但不使用(仅用于初始化包)
import "myproject/mypackage" // 导入本地包
import "github.com/user/package" // 导入远程包
一些注意事项:
- 导入的包路径要使用双引号括起来。
- 导入的包名通常与包的目录名相同,但也可以在包内使用
package
关键字指定不同的包名。 - 如果导入了包但未使用,编译器会报错。可以使用空白标识符
_
来导入包但不使用其内容,仅用于初始化该包。 - 包的导入路径可以是相对路径(本地包)或绝对路径(远程包)。
- 一个 Go 源文件必须在其
package
声明之后立即导入所需的包。
参考:A Tour of Go