Go语言中的面向对象编程
- 可见性控制
publlic 常量、变量、类型、接口、结构、函数等的名称大写
private 非大写就只能在包内使用 - 继承
通过组合实现,内嵌一个或多个struct - 多态
通过接口实现,通过接口定义方法集,编写多套实现
json 编解码
- json.Unmarshal():从string转换至struct
- json.Marshal():从struct转换至string
func unmarshal2Struct(humanStr string) Human {
h := Human{}
err := json.Unmarshal([]byte(humanStr), &h)
if err != nil {
println(err)
}
return h
}
func marshal2JsonString(h Human) string {
h.Age = 30
updatedBytes, err := json.Marshal(&h)
if err != nil {
println(err)
}
return string(updatedBytes)
}
- json包使用map[string]interface{} 和 []interface{} 类型保存任意对象
- 可通过如下逻辑解析人意json
var obj interface{}
err := json.Unmarshal([]byte(humanStr), &obj)
objMap, ok := obj.(map[string]interface{})
for k, v := range objMap {
switch value := v.(type) {
case string:
fmt.Printf("type of %s is string, value is %v\n, k, value)
case interface{}:
fmt.Printf("type of %s is interface{}, value is %v\n, k, value)
default:
fmt.Printf("type of %s is wrong, value is %v\n, k, value)
}
}
错误处理
- Go 语言无内置 exception 机制,只提供error接口定义错误
type error interface {
Error() string
}
- 可通过 errors.New 或 fmt.Errorf 创建新的 error
var errorNotFound error = errors.New("NotFound")
- 通常应用程序对error的处理大部分是判断 error 是否为 nil
如需将error归类,通常交给应用程序自定义,比如kubernetes自定义了与apiserver交互的不同类型错误
type StatusError struct {
ErrStatus metav1.Status
}
var _error = &StatusError{}
// Error implements the Error interface
func (e *StatusError) Error() string {
return e.ErrStatus.Message
}
defer
- 函数返回之前执行某个语句或函数,等同于java和c#的finally
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
time.Sleep(time.Second)
}
// 321 类似于一个栈,反序执行
- 常见的defer使用场景:记得关闭你打开的资源
defer file.Close()
defer mu.Unlock()
defer println("") - 循环死锁
lock := sync.Mutex{}
for i := 0; i < 3; i++ {
lock.Lock() // 第一次循环之后,defer压栈,第二次循环时锁没有释放
defer lock.Unlock()
fmt.Println("loopFunc:", i)
}
- 解决循环死锁
lock := sync.Mutex{}
for i := 0; i < 3; i++ {
go func(i int) { // 使用闭包,让函数推出,释放锁
lock.Lock()
defer lock.Unlock() // 如果不用defer,而该做lock.Unlock()时,一旦逻辑出现错误,程序会报错,lock.Unlock()不会执行,锁资源就不会释放,所以defer可以用来确保锁资源一定会被释放掉
fmt.Println("loopFunc:", i)
}(i)
}
// 321
panic 和 recover
- panic可在系统出现不可恢复错误时主动调用panic,panic会使当前线程crash
- defer保证执行并把控制权交还给接收到panic的函数调用者
- recover,函数从panic或错误场景中恢复
defer func() {
fmt.Println("defer func is called")
if err := recover(); err != nil {
fmt.Println(err)
}()
}
panic("a panic is triggered")
defer一定是和recover配合使用,defer用来保证panic以后,关联的代码仍然可执行,防止线程的crash导致整个进程crash,recover保证从当前的panic状态恢复过来。一般来说,panic错误需要recover。
多线程
并发和并行
- 并发:两个或多个事件在同一时间段内交替发生(多个线程在同一CPU上来回切换、交替执行)
- 并行:两个或多个事件在同一时刻发生(多个CPU同时运行多个线程)
协程
- 进程:分配系统资源(CPU时间、内存等)的基本单位;有独立的内存空间,切换开销大
- 线程:线程的一个执行流,是CPU调度并能独立运行的基本单位;多线程通信方便;从内核层面来看线程其实也是一种特殊的进程,它跟父进程共享了打开的文件和文件系统信息,共享了地址空间和信号处理函数;
- 协程:Go语言中的轻量级线程实现;Golang在runtime、系统调用等多方面对goroutine调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前的goroutine的CPU(P)转让出去,让其他的goroutine能被调度并执行,也就是Golang从语言层面支持了协程;
备注: 进程是系统分配资源的基本单位,线程是CPU调度的基本单位,一个进程可以启动多个线程、从而使用多个CPU进行计算;进程和线程都在操作系统上体现的;
CSP
- Go语言多线程的模型叫做CSP(communicating sequential process),是描述两个(多个)独立的并发实体通过共享的通讯channel进行通信的并发模型
- Go 协程 goroutine 是一种轻量级线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度;是一种绿色线程,微线程,它与coroutine协程也有区别,能够在发现堵塞后启动新的微线程
- 通道channel,类似Unix 的 Pipe,用于协程之间的通讯和同步;协程之间虽然解耦,但是它们和channel有着耦合
线程和协程的差异
- 每个goroutine(协程)默认占用内存远比Java、C的线程少 • goroutine:2KB 线程:8MB
- 线程goroutine切换开销方面,goroutine远比线程小;线程涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新;goroutine:只有三个寄存器的值修改 - PC / SP / DX;
- GOMAXPROCS:控制并行线程数量
协程示例
func main() {
loopFunc()
time.Sleep(time.Second)
}
func loopFunc() {
for i := 0; i < 3; i++ {
// 如果不加goroutine,是在一个CPU上处理的,
// 输出012,加了goroutine,就在多个CPU上处理,
// 并且谁先执行,谁后执行就说不好了,输出结果会乱掉,
// 可能是102
go fmt.Println("loopFunc:", i) // 加了go,就是告诉go语言,这段程序需要启动一个新的线程
}
}
channel 多线程通信
- channel是多个协程之间通信的管道;一端发送数据,一端接收数据;同一时间只有一个协程可以访问数据,无共享内存模式可能出现的内存竞争问题;可以协调协程执行的顺序;
// 示例
ch := make(chan int)
go func() {
fmt.Println("hello from goroutine")
ch <- 0 // 数据写入channel
}()
i := <-ch // 从channel中取数据并赋值
- Go语言可以理解成自带生产者和消费者的支持
通道缓冲
- 基于channel的通信是同步的,当前缓冲区满时,数据的发送是阻塞的;通过make关键字创建通道时可定义缓冲区容量,默认缓冲区容量为0
ch := make(chan int) // 如果没有接收,会阻塞,因为默认缓冲区为0
ch := make(chan int, 1)
- 使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道
- 只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量
- 当向通道中发送完数据时,我们可以通过close函数来关闭通道
- 通道无需每次都关闭
- 关闭的作用是告诉接收者该通道再无新数据发送
- 只有发送方需要关闭通道
- 通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的
- 关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
- 只有发送方需要关闭通道
ch := make(chan int)
defer close(ch) // 重点关注这里
if v, notClosed := <-ch; notClosed {
fmt.Println(v)
}
- 判断通道关闭的两种方式:
// 方式一
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 方式二
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
// 通常使用的是for range的方式。使用for range遍历通道,当通道被关闭的时候就会退出for range
- 单向通道:有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收;Go语言中提供了单向通道来处理这种情况
// chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
// <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。
// 在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不可以的。
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
- 生产者-消费者示例
var c = make(chan int)
go prod(c)
go consume(c)
func prod(ch chan<- int) { // 把双向通道转换成单向通道
for {ch<-}
}
func consume(ch <-chan int) {// 把双向通道转换成单向通道
for {<-ch}
}
select
-当多个协程同时运行时,可通过select轮询多个通道,如果所有通道都堵塞则等待,如定义了default则执行default;如果多个通道就绪则随机选择;
在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:
for{
// 尝试从ch1接收值
data, ok := <-ch1
// 尝试从ch2接收值
data, ok := <-ch2
…
}
这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。
select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
举个小例子来演示下select的使用:
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}
使用select语句能提高代码的可读性。
- 可处理一个或多个channel的发送/接收操作。
- 如果多个case同时满足,select会随机选择一个。
- 对于没有case的select{}会一直等待,可用于阻塞main函数。
定时器 Timer
- time.Ticker 以指定的时间间隔重复的向通道C发送时间值;使用场景:为协程设定超时时间
timer := time.NewTimer(time.Second)
select {
case <-ch:
fmt.Println("received from ch")
case <- timer.C
fmt.Println("timeout waiting from channel ch")
}