匿名方法
匿名方法写法:func (x){...}()
,不用指定名称,所以叫匿名方法
匿名方法其实类似于一个类型实例,匿名方法声明后可以用来赋值和传参,也可以直接使用()运行
结构体标签
为结构体的字段携带元数据的方式,使用反引号包裹,定义为字符串字面量,支持在字段后一次定义多个,空格隔开
go编译器不会对其进行额外转义等处理,而go运行时本身也不会使用该内容,而是第三方库在通过反射等方式进行使用
定义方式是在结构体的具体变量后面,对字段进行描述的场景下尤其有用,写法如下:
type User struct {
// 导出为 JSON 时是 "first_name",导出为 YAML 时是 "firstName"
// 并不是说后续使用时go运行时会自动帮助转换,而是被三方库解析和处理使用
FirstName string `json:"first_name" yaml:"firstName"`
}
常用约定是反引号内的格式通常是k:v
的形式,即冒号隔开的键值对,因为元数据的表达方式通常也是键值对,所以k:v这种形式的结构体标签会很常见
go协程和信道
go协程
官方说法是:由 Go 运行时管理的轻量级线程
启动一个协程:go f(x, y, z)
, 注意:f
, x
, y
和 z
的求值发生在当前的 Go 协程中,而 f
的执行发生在新的 Go 协程中 ,理解这句话很重要,只有f函数的代码执行被放在了新协程中,而参数的求值和响应值求值(没错,响应值返回出来时也会求值)都发生在当前go协程中
不同协程间的通信使用信道完成,可以避免其他语言中复杂的锁机制
信道
信道是用来在不同的协程之间通信的,其概念类似于一个数据传送带,遵循先进先出
箭头方向表达了数据流的方向
将值发送到信道:ch <- v
从信道接收值:v := <-ch,操作符是紧挨着信道值的
信道使用make
函数创建:ch := make(chan T),chan关键字修饰一个类型,表示该信道只为该类型工作,不允许放其他类型值
信道操作符只有:<-
,不存在其他方式比如-> -<
之类的
信道最重要的特点是内置同步:无缓冲信道的话,接收端如果没有在接收(可能阻塞了),那么发送端也不会进行实际发送操作,也会阻塞等待,直到接收端重新开始接收。有缓冲信道的话也是会发满缓冲区,就不再继续发送了,直到缓冲区再次有新的空间
信道分两种:无缓冲信道:ch := make(chan T),有缓冲信道:ch := make(chan T, 5)
有缓冲的信道就相当于发送端和接收端中间放了一个操作台用来中转数据,即使接收端阻塞了,发送端也会发送直到发满数据到缓冲区(这里是5条数据)。
有缓冲信道,当信道的缓冲区填满后,向其发送数据时会阻塞,当缓冲区为空时,接受方会阻塞
信道还有进阶的用法,可以作为函数的参数声明时,限制函数体内只能发还是只能接:
//这个函数只能向信道发送数据,不能接收
func sendData(ch chan<- string, data string) { ch <- data }
//这个函数只能从信道接收数据,不能发送
func receiveData(ch <-chan string) { 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)
}
//报错:
fatal error: all goroutines are asleep - deadlock!
信道缓冲区如果没数据,但是接收端却在主动发起接收,就会报错
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
//ch <- 2 //注释这一行后,运行时也会报错
fmt.Println(<-ch)
fmt.Println(<-ch)
}
//报错:
1
fatal error: all goroutines are asleep - deadlock!
接收者端可以使用双重赋值来接受第二个参数来检查信道是否已被关闭:
v, isOpen := <-ch
关闭信道只允许发送者来关闭,接收者发起关闭的话会导致panic
range
可以不断的从信道中取值,直到信道被关闭
信道并不是必须要关闭的,只有在需要告诉接收者不再有需要接收的值是才有必要关闭,例如停止一个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语句写法类似switch,主要作用是可以使一个协程可以等待多个通信操作。
select是哪个分支就绪就执行哪个,都就绪则会随机选择一个分支执行
select写法:
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
select 的分支都没有准备好时,就会运行default分支,使用default分支可以避免发送或接收时产生阻塞
select示例代码,有效说明了select常规的工作机制:
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.
.
.
tick.
BOOM!
package main
import (
"fmt"
"golang.org/x/tour/tree"
)
// 递归的中序遍历函数
func Walk0(t *tree.Tree, ch chan int) {
if t == nil {
return
}
Walk0(t.Left, ch)
ch <- t.Value
Walk0(t.Right, ch)
}
// Walk 遍历树 t,并树中所有的值发送到信道 ch。
func Walk(t *tree.Tree, ch chan int) {
Walk0(t, ch)
close(ch)
}
// Same 判断 t1 和 t2 是否包含相同的值。
func Same(t1, t2 *tree.Tree) bool {
//创建两个管道同时接收值,并且在循环中同时取值
ch1 := make(chan int)
ch2 := make(chan int)
go Walk(t1, ch1)
go Walk(t2, ch2)
for {
a, ok1 := <-ch1
b, ok2 := <-ch2
//如果没有同时结束或者a不等于b,就说明结构或值有不同的地方,直接返回false
if ok1 != ok2 || a != b {
return false
}
if ok1 == false {
break
}
}
return true
}
func main() {
ch := make(chan int)
go Walk(tree.New(1), ch)
for item := range ch {
fmt.Println(item)
}
t1 := tree.New(1)
t2 := tree.New(1)
t3 := tree.New(2)
fmt.Printf("t1 和 t2 是否相同:%v\n", Same(t1, t2))
fmt.Printf("t1 和 t3 是否相同:%v\n", Same(t1, t3))
}
互斥锁使用sync的sync.Mutex
实现,只需要对应的结构体持有这个锁实例就能实现并发安全的访问:mu sync.Mutex
,因为库的缘故,实际上无需初始化就能使用。在函数中上锁时调用 mu.Lock(),解锁时使用mu.Unlock(),配合defer关键字来保证该锁一定会被解锁