如下的例子,要打印100以內(nèi)能被5整除的數(shù),以panic的方式選擇出來并打印。
創(chuàng)新互聯(lián)公司長期為成百上千家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺(tái),與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為新樂企業(yè)提供專業(yè)的網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì),新樂網(wǎng)站改版等技術(shù)服務(wù)。擁有10年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。
如果用下面的方式,執(zhí)行到第一個(gè)panic就會(huì)跳出for循環(huán)
只能輸出第一個(gè)匹配項(xiàng),然后退出for循環(huán)。
那么如何保證在for循環(huán)處理完panic不退出循環(huán),直到打印完所有滿足條件的數(shù)值?
golang的panic被恢復(fù)后,能繼續(xù)執(zhí)行比recover更早的defer,或者返回到recover函數(shù)的調(diào)用方,然后繼續(xù)執(zhí)行下去。
所以,我們可以把panic和recover放到單獨(dú)的函數(shù)中,然后在for循環(huán)里調(diào)用這個(gè)函數(shù),這個(gè)函數(shù)panic并恢復(fù)后,能返回到調(diào)用方for循環(huán)并繼續(xù)循環(huán)下去。
執(zhí)行結(jié)果是所有0到100的所有符合panic條件的都能正確處理,for循環(huán)沒有異常退出:
golang的panic屬于非常嚴(yán)重的錯(cuò)誤,一旦panic沒有recover的話,程序就退出了。一般避免主動(dòng)panic,影響程序穩(wěn)定性。
recover函數(shù)要放在defer里面,并且只能恢復(fù)同一個(gè)goroutine的并且是直接調(diào)用鏈函數(shù)發(fā)生的panic。recover不能恢復(fù)上一層函數(shù)的panic。
1. 部署簡單
Go
編譯生成的是一個(gè)靜態(tài)可執(zhí)行文件,除了glibc外沒有其他外部依賴。這讓部署變得異常方便:目標(biāo)機(jī)器上只需要一個(gè)基礎(chǔ)的系統(tǒng)和必要的管理、監(jiān)控工具,完全不需要操心應(yīng)用所需的各種包、庫的依賴關(guān)系,大大減輕了維護(hù)的負(fù)擔(dān)。
2. 并發(fā)性好
Goroutine和channel使得編寫高并發(fā)的服務(wù)端軟件變得相當(dāng)容易,很多情況下完全不需要考慮鎖機(jī)制以及由此帶來的各種問題。單個(gè)Go應(yīng)用也能有效的利用多個(gè)CPU核,并行執(zhí)行的性能好。
3. 良好的語言設(shè)計(jì)
從學(xué)術(shù)的角度講Go語言其實(shí)非常平庸,不支持許多高級的語言特性;但從工程的角度講,Go的設(shè)計(jì)是非常優(yōu)秀的:規(guī)范足夠簡單靈活,有其他語言基礎(chǔ)的程序員都能迅速上手。更重要的是
Go 自帶完善的工具鏈,大大提高了團(tuán)隊(duì)協(xié)作的一致性。
4. 執(zhí)行性能好
雖然不如 C 和 Java,但相比于其他編程語言,其執(zhí)行性能還是很好的,適合編寫一些瓶頸業(yè)務(wù),內(nèi)存占用也非常省。
一般命令
所謂一般命令,就是在一定時(shí)間內(nèi)會(huì)執(zhí)行完的命令。比如 grep, cat 等等。 執(zhí)行命令的步驟是:連接,執(zhí)行,獲取結(jié)果
連接
連接包含了認(rèn)證,可以使用 password 或者 sshkey 2種方式來認(rèn)證。下面的示例為了簡單,使用了密碼認(rèn)證的方式來完成連接。
import (
"fmt"
"time"
"golang.org/x/crypto/ssh"
)
func connect(user, password, host string, port int) (*ssh.Session, error) {
var (
auth []ssh.AuthMethod
addr string
clientConfig *ssh.ClientConfig
client *ssh.Client
session *ssh.Session
err error
)
// get auth method
auth = make([]ssh.AuthMethod, 0)
auth = append(auth, ssh.Password(password))
clientConfig = ssh.ClientConfig{
User: user,
Auth: auth,
Timeout: 30 * time.Second,
}
// connet to ssh
addr = fmt.Sprintf("%s:%d", host, port)
if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
return nil, err
}
// create session
if session, err = client.NewSession(); err != nil {
return nil, err
}
return session, nil
}
連接的方法很簡單,只要提供登錄主機(jī)的 用戶*, *密碼*, *主機(jī)名或者IP*, *SSH端口
執(zhí)行,命令獲取結(jié)果
連接成功后,執(zhí)行命令很簡單
import (
"fmt"
"log"
"os"
"time"
"golang.org/x/crypto/ssh"
)
func main() {
session, err := connect("root", "xxxxx", "127.0.0.1", 22)
if err != nil {
log.Fatal(err)
}
defer session.Close()
session.Run("ls /; ls /abc")
}
上面代碼運(yùn)行之后,雖然命令正常執(zhí)行了,但是沒有正常輸出的結(jié)果,也沒有異常輸出的結(jié)果。 要想顯示結(jié)果,需要將 session 的 Stdout 和 Stderr 重定向 修改 func main 為如下:
func main() {
session, err := connect("root", "xxxxx", "127.0.0.1", 22)
if err != nil {
log.Fatal(err)
}
defer session.Close()
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Run("ls /; ls /abc")
}
這樣就能在屏幕上顯示正常,異常的信息了。
交互式命令
上面的方式無法遠(yuǎn)程執(zhí)行交互式命令,比如 top , 遠(yuǎn)程編輯一個(gè)文件,比如 vi /etc/nginx/nginx.conf 如果要支持交互式的命令,需要當(dāng)前的terminal來接管遠(yuǎn)程的 PTY。
func main() {
session, err := connect("root", "olordjesus", "dockers.iotalabs.io", 2210)
if err != nil {
log.Fatal(err)
}
defer session.Close()
fd := int(os.Stdin.Fd())
oldState, err := terminal.MakeRaw(fd)
if err != nil {
panic(err)
}
defer terminal.Restore(fd, oldState)
// excute command
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin
termWidth, termHeight, err := terminal.GetSize(fd)
if err != nil {
panic(err)
}
// Set up terminal modes
modes := ssh.TerminalModes{
ssh.ECHO: 1, // enable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
// Request pseudo terminal
if err := session.RequestPty("xterm-256color", termHeight, termWidth, modes); err != nil {
log.Fatal(err)
}
session.Run("top")
}
這個(gè)問題說來話長,我先表達(dá)一下我的觀點(diǎn),Go語言從語法層面提供區(qū)分錯(cuò)誤和異常的機(jī)制是很好的做法,比自己用單個(gè)返回值做值判斷要方便很多。
上面看到很多知乎大牛把異常和錯(cuò)誤混在一起說,有認(rèn)為Go沒有異常機(jī)制的,有認(rèn)為Go純粹只有異常機(jī)制的,我覺得這些觀點(diǎn)都太片面了。
具體對于錯(cuò)誤和異常的討論,我轉(zhuǎn)發(fā)一下前陣子寫的一篇日志拋磚引玉吧。
============================
最近連續(xù)遇到朋友問我項(xiàng)目里錯(cuò)誤和異常管理的事情,之前也多次跟團(tuán)隊(duì)強(qiáng)調(diào)過錯(cuò)誤和異常管理的一些概念,所以趁今天有動(dòng)力就趕緊寫一篇Go語言項(xiàng)目錯(cuò)誤和異常管理的經(jīng)驗(yàn)分享。
首先我們要理清:什么是錯(cuò)誤、什么是異常、為什么需要管理。然后才是怎樣管理。
錯(cuò)誤和異常從語言機(jī)制上面講,就是error和panic的區(qū)別,放到別的語言也一樣,別的語言沒有error類型,但是有錯(cuò)誤碼之類的,沒有panic,但是有throw之類的。
在語言層面它們是兩種概念,導(dǎo)致的是兩種不同的結(jié)果。如果程序遇到錯(cuò)誤不處理,那么可能進(jìn)一步的產(chǎn)生業(yè)務(wù)上的錯(cuò)誤,比如給用戶多扣錢了,或者進(jìn)一步產(chǎn)生了異常;如果程序遇到異常不處理,那么結(jié)果就是進(jìn)程異常退出。
在項(xiàng)目里面是不是應(yīng)該處理所有的錯(cuò)誤情況和捕捉所有的異常呢?我只能說,你可以這么做,但是估計(jì)效果不會(huì)太好。我的理由是:
如果所有東西都處理和記錄,那么重要信息可能被淹沒在信息的海洋里。
不應(yīng)該處理的錯(cuò)誤被處理了,很容易導(dǎo)出BUG暴露不出來,直到出現(xiàn)更嚴(yán)重錯(cuò)誤的時(shí)候才暴露出問題,到時(shí)候排查就很困難了,因?yàn)橐呀?jīng)不是錯(cuò)誤的第一現(xiàn)場。
所以錯(cuò)誤和異常最好能按一定的規(guī)則進(jìn)行分類和管理,在第一時(shí)間能暴露錯(cuò)誤和還原現(xiàn)場。
對于錯(cuò)誤處理,Erlang有一個(gè)很好的概念叫速錯(cuò),就是有錯(cuò)誤第一時(shí)間暴露它。我們的項(xiàng)目從Erlang到Go一直是沿用這一設(shè)計(jì)原則。但是應(yīng)用這個(gè)原則的前提是先得區(qū)分錯(cuò)誤和異常這兩個(gè)概念。
錯(cuò)誤和異常上面已經(jīng)提到了,從語言機(jī)制層面比較容易區(qū)分它們,但是語言取決于人為,什么情況下用錯(cuò)誤表達(dá),什么情況下用異常表達(dá),就得有一套規(guī)則,否則很容易出現(xiàn)全部靠異常來做錯(cuò)誤處理的情況,似乎Java項(xiàng)目特別容易出現(xiàn)這樣的設(shè)計(jì)。
這里我先假想有這樣一個(gè)業(yè)務(wù):游戲玩家通過購買按鈕,用銅錢購買寶石。
在實(shí)現(xiàn)這個(gè)業(yè)務(wù)的時(shí)候,程序邏輯會(huì)進(jìn)一步分化成客戶端邏輯和服務(wù)端邏輯,客戶端邏輯又進(jìn)一步因?yàn)樵O(shè)計(jì)方式的不同分化成兩種結(jié)構(gòu):胖客戶端結(jié)構(gòu)、瘦客戶端結(jié)構(gòu)。
胖客戶端結(jié)構(gòu),有更多的本地?cái)?shù)據(jù)和懂得更多的業(yè)務(wù)邏輯,所以在胖客戶端結(jié)構(gòu)的應(yīng)用中,以上的業(yè)務(wù)會(huì)實(shí)現(xiàn)成這樣:客戶端檢查緩存中的銅錢數(shù)量,銅錢數(shù)量足夠的時(shí)候購買按鈕為可用的亮起狀態(tài),用戶點(diǎn)擊購買按鈕后客戶端發(fā)送購買請求到服務(wù)端;服務(wù)端收到請求后校驗(yàn)用戶的銅錢數(shù)量,如果銅錢數(shù)量不足就拋出異常,終止請求過程并斷開客戶端的連接,如果銅錢數(shù)量足夠就進(jìn)一步完成寶石購買過程,這里不繼續(xù)描述正常過程。
因?yàn)檎5目蛻舳耸怯幸徊綌?shù)據(jù)校驗(yàn)的過程的,所以當(dāng)服務(wù)端收到不合理的請求(銅錢不足以購買寶石)時(shí),拋出異常比返回錯(cuò)誤更為合理,因?yàn)檫@個(gè)請求只可能來自兩種客戶端:外掛或者有BUG的客戶端。如果不通過拋出異常來終止業(yè)務(wù)過程和斷開客戶端連接,那么程序的錯(cuò)誤就很難被第一時(shí)間發(fā)現(xiàn),攻擊行為也很難被發(fā)現(xiàn)。
我們再回頭看瘦客戶端結(jié)構(gòu)的設(shè)計(jì),瘦客戶端不會(huì)存有太多狀態(tài)數(shù)據(jù)和用戶數(shù)據(jù)也不清楚業(yè)務(wù)邏輯,所以客戶端的設(shè)計(jì)會(huì)是這樣:用戶點(diǎn)擊購買按鈕,客戶端發(fā)送購買請求;服務(wù)端收到請求后檢查銅錢數(shù)量,數(shù)量不足就返回?cái)?shù)量不足的錯(cuò)誤碼,數(shù)量足夠就繼續(xù)完成業(yè)務(wù)并返回成功信息;客戶端收到服務(wù)端的處理結(jié)果后,在界面上做出反映。
在這種結(jié)構(gòu)下,銅錢不足就變成了業(yè)務(wù)邏輯范圍內(nèi)的一種失敗情況,但不能提升為異常,否則銅錢不足的用戶一點(diǎn)購買按鈕都會(huì)出錯(cuò)掉線。
所以,異常和錯(cuò)誤在不同程序結(jié)構(gòu)下是互相轉(zhuǎn)換的,我們沒辦法一句話的給所有類型所有結(jié)構(gòu)的程序一個(gè)統(tǒng)一的異常和錯(cuò)誤分類規(guī)則。
但是,異常和錯(cuò)誤的分類是有跡可循的。比如上面提到的痩客戶端結(jié)構(gòu),銅錢不足是業(yè)務(wù)邏輯范圍內(nèi)的一種失敗情況,它屬于業(yè)務(wù)錯(cuò)誤,再比如程序邏輯上嘗試請求某個(gè)URL,最多三次,重試三次的過程中請求失敗是錯(cuò)誤,重試到第三次,失敗就被提升為異常了。
所以我們可以這樣來歸類異常和錯(cuò)誤:不會(huì)終止程序邏輯運(yùn)行的歸類為錯(cuò)誤,會(huì)終止程序邏輯運(yùn)行的歸類為異常。
因?yàn)殄e(cuò)誤不會(huì)終止邏輯運(yùn)行,所以錯(cuò)誤是邏輯的一部分,比如上面提到的瘦客戶端結(jié)構(gòu),銅錢不足的錯(cuò)誤就是業(yè)務(wù)邏輯處理過程中需要考慮和處理的一個(gè)邏輯分支。而異常就是那些不應(yīng)該出現(xiàn)在業(yè)務(wù)邏輯中的東西,比如上面提到的胖客戶端結(jié)構(gòu),銅錢不足已經(jīng)不是業(yè)務(wù)邏輯需要考慮的一部分了,所以它應(yīng)該是一個(gè)異常。
錯(cuò)誤和異常的分類需要通過一定的思維訓(xùn)練來強(qiáng)化分類能力,就類似于面向?qū)ο蟮脑O(shè)計(jì)方式一樣的,技術(shù)實(shí)現(xiàn)就擺在那邊,但是要用好需要不斷的思維訓(xùn)練不斷的歸類和總結(jié),以上提到的歸類方式希望可以作為一個(gè)參考,期待大家能發(fā)現(xiàn)更多更有效的歸類方式。
接下來我們講一下速錯(cuò)和Go語言里面怎么做到速錯(cuò)。
速錯(cuò)我最早接觸是在做的時(shí)候就體驗(yàn)到的,當(dāng)然跟Erlang的速錯(cuò)不完全一致,那時(shí)候也沒有那么高大上的一個(gè)名字,但是對待異常的理念是一樣的。
在.NET項(xiàng)目開發(fā)的時(shí)候,有經(jīng)驗(yàn)的程序員都應(yīng)該知道,不能隨便re-throw,就是catch錯(cuò)誤再拋出,原因是異常的第一現(xiàn)場會(huì)被破壞,堆棧跟蹤信息會(huì)丟失,因?yàn)橥獠孔詈竽玫疆惓5亩褩8櫺畔ⅲ亲詈竽谴蝨hrow的異常的堆棧跟蹤信息;其次,不能隨便try catch,隨便catch很容易導(dǎo)出異常暴露不出來,升級為更嚴(yán)重的業(yè)務(wù)漏洞。
到了Erlang時(shí)期,大家學(xué)到了速錯(cuò)概念,簡單來講就是:讓它掛。只有掛了你才會(huì)第一時(shí)間知道錯(cuò)誤,但是Erlang的掛,只是Erlang進(jìn)程的異常退出,不會(huì)導(dǎo)致整個(gè)Erlang節(jié)點(diǎn)退出,所以它掛的影響層面比較低。
在Go語言項(xiàng)目中,雖然有類似Erlang進(jìn)程的Goroutine,但是Goroutine如果panic了,并且沒有recover,那么整個(gè)Go進(jìn)程就會(huì)異常退出。所以我們在Go語言項(xiàng)目中要應(yīng)用速錯(cuò)的設(shè)計(jì)理念,就要對Goroutine做一定的管理。
在我們的游戲服務(wù)端項(xiàng)目中,我把Goroutine按掛掉后的結(jié)果分為兩類:1、掛掉后不影響其他業(yè)務(wù)或功能的;2、掛掉后業(yè)務(wù)就無法正常進(jìn)行的。
第一類Goroutine典型的有:處理各個(gè)玩家請求的Goroutine,因?yàn)槊總€(gè)玩家連接各自有一個(gè)Goroutine,所以掛掉了只會(huì)影響單個(gè)玩家,不會(huì)影響整體業(yè)務(wù)進(jìn)行。
第二類Goroutine典型的有:數(shù)據(jù)庫同步用的Goroutine,如果它掛了,數(shù)據(jù)就無法同步到數(shù)據(jù)庫,游戲如果繼續(xù)運(yùn)行下去只會(huì)導(dǎo)致數(shù)據(jù)回檔,還不如讓整個(gè)游戲都異常退出。
這樣一分類,就可以比較清楚哪些Goroutine該做recover處理,哪些不該做recover處理了。
那么在做recover處理時(shí),要怎樣才能盡量保留第一現(xiàn)場來幫組開發(fā)者排查問題原因呢?我們項(xiàng)目中通常是會(huì)在最外層的recover中把錯(cuò)誤和堆棧跟蹤信息記進(jìn)日志,同時(shí)把關(guān)鍵的業(yè)務(wù)信息,比如:用戶ID、來源IP、請求數(shù)據(jù)等也一起記錄進(jìn)去。
為此,我們還特地設(shè)計(jì)了一個(gè)庫,用來格式化輸出堆棧跟蹤信息和對象信息,項(xiàng)目地址:funny/debug · GitHub
通篇寫下來發(fā)現(xiàn)比我預(yù)期的長很多,所以這里我做一下歸納總結(jié),幫組大家理解這篇文章所要表達(dá)的:
錯(cuò)誤和異常需要分類和管理,不能一概而論
錯(cuò)誤和異常的分類可以以是否終止業(yè)務(wù)過程作為標(biāo)準(zhǔn)
錯(cuò)誤是業(yè)務(wù)過程的一部分,異常不是
不要隨便捕獲異常,更不要隨便捕獲再重新拋出異常
Go語言項(xiàng)目需要把Goroutine分為兩類,區(qū)別處理異常
在捕獲到異常時(shí),需要盡可能的保留第一現(xiàn)場的關(guān)鍵數(shù)據(jù)
以上僅為一家之言,拋磚引玉,希望對大家有所幫助。