- 函数和方法:
- 定义函数:
func split(sum int) (x, y int) {
- 定义结构体的方法:
func (v Vertex) Abs() float64 {
- 定义函数:
# 教程
A Tour of Go (opens new window)
# 变量声明
var x int // x is int
var p *int // p is pointer of int
var a [3]int // a is array[3] of int
var (
x int // x is int
p *int // p is pointer of int
a [3]int // a is array[3] of int
)
var x int = 1
var p *int = &x
var x = 1
var p = &x
x := 1
p := &x
const Pi = 3.14
# Go 奇异的变量声明方式
第一眼看到,觉得很怪异。不过看了这篇 关于 Go 语法声明的文章 (opens new window),觉得挺有意思。
Go 的声明语句是为了和自然语言(英语)保持一致:
x int // x is int
p *int // p is pointer of int
a [3]int // a is array[3] of int
这主要针对的是 C 的指针,特别是加入了函数指针,一切都复杂起来:
int (*(*fp)(int (*)(int, int), int))(int, int)
这段代码定义了一个函数指针 fp
,其接收两个参数,第一个是一个函数指针(类型是 int (*)(int, int)
,接收两个 int 并返回 int),第二个参数是 int
。fp
返回一个函数指针,类型是最外层的 int (*(...))(int, int)
,表示接收两个 int 并返回 int)。
如果用 Go 改写,将会变成:
f := func(func (int, int) int, int) func(int, int) int
按照从左往右以 函数接收 xx 参数,返回 xx
的形式解读,可以知道:f
是一个函数,其接收两个参数,第一个参数是一个函数(func (int, int) int
,接收。两个 int 并返回 int),第二个参数是一个 int。返回类型是一个函数 func(int, int) int
,表示接收两个 int 并返回一个 int。
这里仅对文章核心作出解释,英文原文有更循序渐进的解释。
# 流程控制
# for
for i:= 0; i < 10; i++ {
sum += i
}
nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
fmt.Printf("%d\n", i)
sum += num
}
for i := range nums {
}
for _, num := range nums {
}
# while
// while
for i < 10 {
sim += i
i++
}
// while true
for {
}
# if
if v < lim {
}
if v := math.Pow(x, n); v < lim {
}
# defer
defer 语句会将函数推迟到外层函数返回之后执行。
func main() {
defer fmt.Printf("world!")
fmt.Printf("hello, ")
}
// hello, world!
也可以写一个匿名函数然后调用它:
func main() {
defer func() { fmt.Printf("world!") }()
fmt.Printf("hello, ")
}
# 更多类型:指针、结构体、切片和映射
# 指针 pointer
i := 42
p = &i
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i
# 结构体 struct
// 结构体
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
p = &v1
)
v1.X
(*p).X
p.X // 等价于 (*p).X
// 硬是把指针玩成了引用
# 数组 array 和切片 slice
// 数组
// 数组的长度是其类型的一部分,因此数组不能改变大小
var a [10]int
// 切片
var s []int
// [low: high], 左闭右开
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4] // [3 5 7]
// 默认 low = 0, high = length
var s []int = primes[:] // [2 3 5 7 11 13]
// 数组
// `类型{}` 可以理解成 Go 的构造函数,后面是构造函数的参数。被称为复合字面量 (Composite literals)
a := [3]bool{true, true, false}
a := [...]bool{true, true, false}
// 创建一个和上面相同的数组,然后构建一个引用了它的切片
s := []bool{true, true, false}
// 切片的长度是 ... 就是它的长度
len(s)
// 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数
// 可能是为了方便的检查 append 操作是否越界
cap(s)
make
用于创建切片/动态数组:
s := make([]int, 3)
// cap(s) = len(s) = 3
append
用于在切片后增加元素。
func append(s []T, vs ...T) []T
append
不会改变原来的切片,所以需要再赋值给s
:s = append(s, 1)
- 如果
s
的底层数组大小足够,append
会直接修改底层数组;否则,会重新分配一个更大的数组,返回的切片会指向这个新数组,原数组不变
Go 切片:用法和本质 - Go 语言博客 (opens new window)
Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。(为了避免复制数组,你可以传递一个指向数组的指针,但是数组指针并不是数组)
一个切片可以看作一个结构体,包含三个元素:指向数组的指针、片段的长度和容量。
# make
Making slices, maps and channels (opens new window)
个人认为 make 是一个非常神奇的存在。
内置函数
make
接受的第一个参数是类型T
(仅限于slice
、map
或channel
)。第二个(可选)参数是一个表达式列表,因T
而异。make
返回一个T
的值(而非指针)。
Call Type T Result
make(T, n) slice slice of type T with length n and capacity n
make(T, n, m) slice slice of type T with length n and capacity m
make(T) map map of type T
make(T, n) map map of type T with initial space for approximately n elements
make(T) channel unbuffered channel of type T
make(T, n) channel buffered channel of type T, buffer size n
可以理解成 make
是一个初始化的工具函数。
# 映射 map
// 一个 string -> Vertex 的映射
// 未初始化,不能直接使用
var m = map[string]Vertex
// make 初始化和复合字面量初始化
var m = make(map[string]Vertex)
var m = map[string]Vertex{}
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
// 初始化以后就可以用了
m["a"] = Vertex{}; // 增
delete(m, "a") // 删
fmt.Println(m["Bell Labs"]) // 查
m["Bell Labs"] = Vertex{40.68433, -74.39968} // 改
# 函数和方法
函数:
// 接受两个参数,返回一个参数
func add(x int, y int) int {
return x + y
}
// 相同类型可以简写
func add(x, y int) int {}
// 可以返回多个值
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
// 返回值可以被命名,赋值后直接 return
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
方法:
type Vertex struct {
X, Y float64
}
// go 的方法是写在类外的,所属的类(被称为接收者)需要在函数前指明
// 但也不是哪里都可以定义方法:类型定义和方法声明必须在同一包内
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// 指针接收者的用法和普通接收者完全一样
// 但可以进行引用传递而不是值传递,这很像 C++ 的引用
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func Scale(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
/*
* 调用方法不需要考虑传指针还是传变量,直接调用就行
* 编译器会自动根据方法签名,将代码解释为 (*v).Scale() 或 (&v).Scale()
*/
v.Scale(10)
Scale(&v, 10) //而普通函数传参就需要写 &v
fmt.Println(v.Abs())
}
# 接口
没学过 Java 的我看 Go 的官方教程 (opens new window)的接口部分看得我一脸懵,于是找了其他的教程,看到菜鸟教程 (opens new window)的示例不错。
package main
import (
"fmt"
)
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 = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
C++ 没有接口的概念,Java 有,可参考 Java 接口 (opens new window)。
如果用 C++ 的方式理解,interface
可以裂解为一个“父类”,它声明了很多“虚函数”,由“子类”靠重载进行实现嘛!所以也就出现 interface
“接口类型可以被赋值”这种听起来匪夷所思的事情,其实也就是把子类值赋给了父类变量,之后便可以调用父类的成员函数了。
这也是 Go 为了弥补没有类而产生的语法吧。
顺便一提,main 函数不使用 interface 写法也是可以的:
func main() {
phone1 := new(NokiaPhone)
phone1.call()
phone2 := new(IPhone)
phone2.call()
}
# 空接口 empty interface
接口还不止于此:一个不包含任何方法的接口被称为空接口 empty interface。
空接口可保存任何类型的值(因为每个类型都至少实现了零个方法)。
空接口被用来处理未知类型的值。例如,fmt.Print
可接受类型为 interface{}
的任意数量的参数。
# 类型断言和类型选择 type assertion and switch
所以又接着产生了类型断言 type assertion
和类型选择 type switch
。
类型断言用于断言这个 interface
里到底是什么东西。其语法及对应输出如下:
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
// hello
s, ok := i.(string)
fmt.Println(s, ok)
// hello true
f, ok := i.(float64)
fmt.Println(f, ok)
// 0 false
f = i.(float64)
fmt.Println(f)
/*
panic: interface conversion: interface {} is string, not float64
goroutine 1 [running]:
main.main()
/tmp/sandbox906950040/prog.go:17 +0x1fe
*/
}
类型选择就是综合了类型断言和 switch
:
switch v := i.(type) {
case int:
// v 的类型为 int
case float64:
// v 的类型为 float64
default:
// 没有匹配,v 与 i 的类型相同
}
# 常用接口 Stringer
fmt
(opens new window) 包中定义的 Stringer
(opens new window) 是最普遍的接口之一。
type Stringer interface {
String() string
}
Stringer
(opens new window) 是一个可以用字符串描述自己的类型。fmt
(opens new window) 包(还有很多包)都通过此接口来打印值。
类似于 Java 的 toString()
,定义了这个函数以后就可以调用 fmt
输出了。
# Go 包
导入包:
import "fmt"
import "math"
import (
"fmt"
"math"
)
大写开头的变量和函数会被自动导出,小写的则不能被导出。
所以,Go 的命名方法是,需要导出的东西使用 PascalCase,而内部的东西使用 camelCase。看起来很怪异,因为这和 C++/Java 的类使用 PascalCase、对象使用 camelCase 不同。
# Go 语言代码风格
go fmt <filename>.go
永远滴神!
虽然 go fmt
的风格是用 tab,还是八个空格的 tab,但至少有一个官方排版方案,所以比 C++、Java、Python 各种民间规范更能让人接受。
# Go 的指针
指针和引用的功能是类似的,所以很多语言语言只实现了指针(如 C、Go),或只实现了引用(如 Java、Python)。如果二者都实现了,可能开发者也偏向于使用单一的一种(如 C++ STL中基本都是使用指针)。
Go 只实现了指针。但是不同的是,它的指针结构体有点意思:(*p).X
可以简写为 p.X
!
这种写法,使得结构体指针访问成员可以写成 p.X
,这种写法反而更像是引用。所以,Go 虽然使用的是指针,但其语法也借鉴了引用的优点。
# Go 并发
# Go 多线程
Go 开多线程也太香了吧,直接 go <function>
就可以了。
# Go 线程同步:信道
Go 的线程同步使用的是信道 channel
,类似于《操作系统——精髓与设计原理》里进程同步的 消息传递 方法。
# 无缓冲区信道
默认的信道是无缓冲区的,这种情况下采用的是“阻塞发送、阻塞接收”的方式:发送方和接收方先准备好的一方会被阻塞,直至另一方也准备好了。
创建一个无缓冲区的 int
信道可以使用 c := make(chan int)
。
// 将求数组和问题分配到两个进程完成(分别计算前 3 个和、后 3 个和)
package main
import "fmt"
func sum(a []int, c chan int) {
s := 0
for _, v := range a {
s += v
}
c <- s // 将和送入 c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c // 从 c 中接收
fmt.Println(x, y, x+y)
}
c
无缓冲区,因此在主线程执行 x, y := <-c, <-c
之前就准备好的 c<-s
的 sum
会被阻塞。
对于无缓冲区的信道,这么写会报死锁:
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1
fmt.Println(<-ch)
}
/*
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox780334017/prog.go:7 +0x59
Program exited.
*/
# 有缓冲区信道
下面这种使用缓冲区信道,则是:当缓冲区满时阻塞发送方、当缓冲区空时阻塞接收方。又像是生产者、消费者问题模型了。
缓冲区大小为 n 的 int
信道定义方法为 ch := make(chan int, n)
。顺便一提,无缓冲区的代码也可以用 ch := make(chan int, 0)
定义。
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
有意思的是,如果加两行,也会报死锁:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
/*
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox780334017/prog.go:7 +0x59
Program exited.
*/
# 源源不断从信道接收值
发送者可通过 close(c)
关闭一个信道,表示没有需要发送的值了。
接收者可以使用 v, ok := <-ch
判断信道是否被关闭:若没有值可以接收且信道已被关闭,那么执行后 ok
会被设置为 false
。
循环 for i := range c
会不断从信道接收值,直到它被关闭。
注意:
- 信道关闭后,之前传进缓冲区的值仍可被接收;
- 信道关闭并没有值可以接收后,再次接收会接收到零值(如果信道没有关闭,该线程会被阻塞);
- 只有发送者才能关闭信道,而接收者不能。因为向一个已经关闭的信道发送数据会引发程序恐慌 (panic)。
- 关闭信道不是必需操作。只有在需要终止一个
range
循环等情况下需要关闭。
// 万能的斐波那契数列
package main
import (
"fmt"
)
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 选择最先就绪的执行
select
会阻塞当前线程,直至某个分支可以继续执行,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
也可以添加 default
,如果当前没有分支可以执行,就不会阻塞当前线程而是执行 default
语句。
package main
import (
"fmt"
"time"
)
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)
}
}
}
输出:
.
.
tick.
.
.
tick.
.
.
tick.
.
.
tick.
.
.
BOOM!
# 互斥锁
互斥方案就类似于《操作系统——精髓与设计原理》里进程同步的信号量了。
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}