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语言Goroutines, Channels, Panics, 和 Errors的粗略分析_错误处理

总有可能返回错误,但对语言来说可能并不必要。`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