Golang 对于 DevOps 之利弊(第 1 部分,共 6 部分):Goroutines, Panics 和 Errors
DevOpsGoogleGo6goroutinespanicserrorsGoDevOps
DevOpsGoogleGo6goroutinespanicsand errorsGoDevOpsGoogleGoDevOpsDevOpsGoogleDevOpsGoogleGoDevOpsPythonGoGoDevOps6Go
- Golang 对于 DevOps 之利弊第一篇:Goroutines, Channels, Panics, and Errors(本篇)
- Golang 对于 DevOps 之利弊第二篇:自动接口实现,共有/私有变量
- Golang 对于 DevOps 之利弊第三篇:速度 VS 缺少泛型
- Golang 对于 DevOps 之利弊第四篇:打包时间与方法重载
- Golang 对于 DevOps 之利弊第五篇:交叉编译,窗口,信号,文档和编译器
- Golang 对于 DevOps 之利弊第六篇:Defer 语句和包依赖版本控制
GoCGoGoGoHere it goes
GoHere it goesgoesgo
Go 语言的好处 1: Goroutines — 轻量,内核级线程
GoroutineGoGoroutineGoroutinePythonGoGILGoroutineGoGoJavaNode.jsDevOpsGo
怎样执行一个 Goroutine
goroutinego
import "time"
func monitorCpu() { … }
func monitorDisk() { … }
func monitorNetwork() { … }
func monitorProcesses() { … }
func monitorIdentity() { … }
func main() {
for !shutdown {
monitorCpu()
monitorDisk()
monitorNetwork()
monitorProcesses()
monitorIdentity()
time.Sleep(5*time.Second)
}
}
CPU5
goroutinegoroutinego
func main() {
for !shutdown {
go monitorCpu()
go monitorDisk()
go monitorNetwork()
go monitorProcesses()
go monitorIdentity()
time.Sleep(5*time.Second)
}
}
现在如果它们之中任何一个挂掉的话,仅仅被阻塞的调用会被终止,不会阻塞其它的监控调用函数。并且因为产生一个线程很容易也很快,现在我们事实上更靠近了每隔 5 秒钟检查这些系统。
goroutinepanicgoroutine
Java12Go2GopanicserrorsGo
同步包(和 Channels)加上编排
Gochannelsgoroutinesync.Mutexsync.WaitGroupshutdownChannel
sync.Mutexsync.WaitGroup
GoCC++`` 的人解释:结构体是值传递的。任何时间你创建一个或者
GoCC++`` 的人解释:结构体是值传递的。任何时间你创建一个或者
type Example struct {
wg *sync.WaitGroup
m *sync.Mutex
}
func main() {
wg := &sync.WaitGroup{}
m := &sync.Mutex{}
}
WaitGroup
import "sync"
…
func main() {
for !shutdown {
wg := &sync.WaitGroup{}
doCall := func(fn func()) {
wg.Add(1)
go func() {
defer wg.Done()
fn()
}
}
doCall(monitorCpu)
doCall(monitorDisk)
doCall(monitorNetwork)
doCall(monitorProcesses)
doCall(monitorIdentity)
wg.Wait()
}
}
然而对于这些结构体的警告恰恰在页面的顶部,很容易忽视掉,这将会导致你将要构建的应用出现奇怪的副作用。
WaitGroup
package main
import (
"fmt"
"sync"
"time"
)
func printAndSleep(m *sync.Mutex, x int) {
m.Lock()
defer m.Unlock()
fmt.Println(x)
time.Sleep(time.Second)
}
func main() {
m := &sync.Mutex{}
for i := 0; i < 10; i++ {
printAndSleep(m, i)
}
}
mutexCPUdefer
package main
import (
"fmt"
"sync"
"time"
)
func printAndSleep(m *sync.Mutex, x int) {
m.Lock()
defer m.Unlock()
fmt.Println(x)
time.Sleep(time.Second)
}
func main() {
m := &sync.Mutex{}
for i := 0; i < 10; i++ {
printAndSleep(m, i)
}
}
goroutine(s)
Goroutines 的自动清理
goroutine(s)goroutinesgoroutinegoroutineIPCIPC channel
package ipc
import (
"bufio"
"bytes"
"fmt"
"os"
"time"
)
type ProtocolReader struct {
Channel chan *ProtocolMessage
reader *bufio.Reader
handle *os.File
}
func NewProtocolReader(handle *os.File) *ProtocolReader {
return &ProtocolReader{
make(chan *ProtocolMessage, 15),
bufio.NewReader(handle),
handle,
}
}
func (this *ProtocolReader) ReadAsync() {
go func() {
for {
line, err := this.reader.ReadBytes('\n')
if err != nil {
this.handle.Close()
close(this.Channel)
return nil
}
message := &ProtocolMessage{}
message.Unmarshal(line)
this.Channel <- message
}
return nil
}
}
第二个例子用来说明主线程退出而忽略正在运行的 goroutine:
package main
import (
"fmt"
"time"
)
func waitAndPrint() {
time.Sleep(time.Second)
fmt.Println("got it!")
}
func main() {
go waitAndPrint()
}
sync.WaitGrouptime.Sleeptime.SleepWaitGroup
Channels
Gochannel(s)GC
numSlots := 5
make(chan int, numSlots)
channelchannelsqueuechannelgoroutineschannelchannel
package main
import (
"fmt"
"sync"
"time"
)
var shutdownChannel = make(chan struct{}, 0)
var wg = &sync.WaitGroup{}
func start() {
wg.Add(1)
go func() {
ticker := time.Tick(100*time.Millisecond)
for shutdown := false; !shutdown; {
select {
case <-ticker:
fmt.Println("tick")
case <-shutdownChannel:
fmt.Println("tock")
shutdown = true
}
}
wg.Done()
}()
}
func stop() {
close(shutdownChannel)
}
func wait() {
wg.Wait()
}
func main() {
start()
time.Sleep(time.Second)
stop()
wait()
}
GoselectGo
Go 语言的糟糕之处 1:Panic 和 Error 的处理
panicerrorGopanicerror
Panic 是一个内建函数,用于终止普通的控制流,并使程序崩溃。当函数 F 引发 panic 时,F 的执行终止,通常函数 F 中的任一 deferred 函数会被执行,并且 F 会返回给它的调用者。对于调用者来说,F 表现得像是一个调用 panic的函数。这个进程继续收回栈帧(栈是向下增长,函数返回时退栈)直到当前 goroutine 中所有的函数返回,这时程序就崩溃了。Panics 可以通过直接调用来引发。它们也可以由运行时错误引发,如数组访问越界。 换句话说,当你遇到一个控制流问题时,panics 终止你的程序。
有几种方式可以触发一个 panic:
Attribute = map["This doesn’t exist"]
errorGo
type error interface {
Error() string
}
根据以上定义,这是对于为什么我们讨厌 Go 拥有 error 和 panic 的总结:
Error 是为了避免异常流,而 panic 抵消了这种作用
对于任何一种编程语言,只要拥有 error 和 panic 其中之一就足够了。至少可以说,一些编程语言兼有二者是令人沮丧的。Go 语言的开发者们不幸地跟错了潮流,错误地选择了兼有二者。
一份在流行编程语言中错误处理的抽样
总有可能返回错误,但对语言来说可能并不必要。`Go` 的很多內建函数,比如访问 `map` 元素、从 `channel` 中读取数据、`JSON` 编码等,都需要用到错误处理。这就使为什么 `Go` 和 其他一些类似的语言接纳 `"error"`这个设计,而像 `Python` 和 `Scala` 等语言却没有。
Go 语言官方博客再次介绍:
errorGo
Gopanicerror
Error 增加你的代码的大小
panicerrorerrorerrorpanicerrorerrorpanicGopanicerrorerrorerrorpanicerrortry/catch
try {
this.downloadModule(moduleSettings)
this.extractModule(moduleSettings)
this.tidyManifestModule(moduleSettings)
this.restartCommand(moduleSettings)
this.cleanupModule(moduleSettings)
return nil
}
catch e {
case Exception => return e
}try {
this.downloadModule(moduleSettings)
this.extractModule(moduleSettings)
this.tidyManifestModule(moduleSettings)
this.restartCommand(moduleSettings)
this.cleanupModule(moduleSettings)
return nil
}
catch e {
case Exception => return e
}
errorerror
对于一个错误,你的代码的每一行都必须这么做:
if err := this.downloadModule(moduleSettings); err != nil {
return err
}
if err := this.extractModule(moduleSettings); err != nil {
return err
}
if err := this.tidyManifestModule(moduleSettings); err != nil {
return err
}
if err := this.restartCommand(currentUpdate); err != nil {
return err
}
if err := this.cleanupModule(moduleSettings); err != nil {
return err
}
在最糟糕的情况下,它将使你的代码库增加至三倍。三倍!不,它不是每一行 - 结构体,接口,导入库和空白行完全不受影响。所有的其他行,你知道的,有实际代码的行,都增至三倍。
errorpanicpanicpanicpanictry/catchtry/catchwrapperpanicerrorgoroutinepanicerror
package safefunc
import (
"common/log"
"common/timeout"
"runtime/debug"
"time"
)
type RetryConfig struct {
MaxTries int
BaseDelay time.Duration
MaxDelay time.Duration
SplayFraction float64
ShutdownChannel <-chan struct{}
}
func DefaultRetryConfig() *RetryConfig {
return &RetryConfig{
MaxTries: -1,
BaseDelay: time.Second,
MaxDelay: time.Minute,
SplayFraction: 0.25,
ShutdownChannel: nil,
}
}
func Retry(name string, config *RetryConfig, callback func() error) {
// this is stupid, but necessary.
// when a function panics, that function's returns are zeros.
// that's the only way to check (can't rely on a nil error during a panic)
var noPanicSuccess int = 1
failedAttempts := 0
wrapped := func() (int, error) {
defer func() {
if err := recover(); err != nil {
log.Warn.Println("Recovered panic inside", name, err)
log.Debug.Println("Panic Stacktrace", string(debug.Stack()))
}
}()
return noPanicSuccess, callback()
}
retryLoop:
for {
wrappedReturn, err := wrapped()
if err != nil {
log.Warn.Println("Recovered error inside", name, err)
log.Debug.Println("Recovered Stacktrace", string(debug.Stack()))
} else if wrappedReturn == noPanicSuccess {
break retryLoop
}
failedAttempts++
if config.MaxTries > 0 && failedAttempts >= config.MaxTries {
log.Trace.Println("Giving up on retrying", name, "after", failedAttempts, "attempts")
break retryLoop
}
sleep := timeout.Delay(config.BaseDelay, failedAttempts, config.SplayFraction, config.MaxDelay)
log.Trace.Println("Sleeping for", sleep, "before continuing retry loop", name)
sleepChannel := time.After(sleep)
select {
case <-sleepChannel:
case <-config.ShutdownChannel:
log.Trace.Println("Shutting down retry loop", name)
break retryLoop
}
}
}
Gopanicgoroutine
并非每人都讨厌 Go 中的 error 和 panic
Blue MatadorerrorGo
panictry/catchpanicerror
我们对 error 和 panic 的主要抱怨
Gotry/catchpanicpanicpanicpanicpanicpanicGoerrorpanicerrorpanicGoGolangDevOps