自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

設(shè)計(jì)Go API的管道使用原則

開發(fā) 前端 后端
管道是并發(fā)安全的隊(duì)列,用于在Go的輕量級(jí)線程(Go協(xié)程)之間安全地傳遞消息??偟膩碇v,這些原語是Go語言中最為稱道的特色功能之一。這種消息傳遞范式使得開發(fā)者可以以易于理解的語義和控制流來協(xié)調(diào)管理多線程并發(fā)任務(wù),而這勝過使用回調(diào)函數(shù)或者共享內(nèi)存。

管道是并發(fā)安全的隊(duì)列,用于在Go的輕量級(jí)線程(Go協(xié)程)之間安全地傳遞消息??偟膩碇v,這些原語是Go語言中最為稱道的特色功能之一。這種消息傳遞范式使得開發(fā)者可以以易于理解的語義和控制流來協(xié)調(diào)管理多線程并發(fā)任務(wù),而這勝過使用回調(diào)函數(shù)或者共享內(nèi)存。

即使管道如此強(qiáng)大,在公有的API中卻不常見。例如,我梳理過Go的標(biāo)準(zhǔn)庫,在145個(gè)包中有超過6000個(gè)公有的API。在這上千個(gè)API中,去重后,只有5個(gè)用到了管道。

在公有的API中使用管道時(shí),如何折衷考慮和取舍,缺乏指導(dǎo)。“共有API”,我是指“任何實(shí)現(xiàn)者和使用者是不同的兩個(gè)人的編程接口”。這篇文章會(huì)深入講解,為如何在共有API中使用管道,提供一系列的原則和解釋。一些特例會(huì)在本章末尾討論。

原則 #1

API應(yīng)該聲明管道的方向性。

例子

time.After

  1. func After(d Duration) <-chan Time 

signal.Notify

  1. func Notify(c chan<- os.Signal, sig ...os.Signal) 

盡管并不常用,Go允許指定一個(gè)管道的方向性。語言規(guī)范這么寫:

可選的<-操作符指定了管道的方向,發(fā)送或接收。如果沒有指定方向,那么管道就是雙向的。

關(guān)鍵在于API簽名中的方向操作符會(huì)被編譯器強(qiáng)制檢查

  1. t := time.After(time.Second)  
  2. t <- time.Now()  // 會(huì)編譯失敗(send to receive-only type <-chan Time) 

除了能夠被編譯器強(qiáng)制檢查安全性,方向操作符還能幫助API使用者理解數(shù)據(jù)的流動(dòng)方向——只需要看一下類型簽名即可。

原則 #2

向一個(gè)管道發(fā)送無界數(shù)據(jù)流的API必須寫文檔解釋清楚在消費(fèi)者消費(fèi)不及時(shí)時(shí)API的行為。

例子

time.NewTicker

  1. // NewTicker returns a new Ticker containing a channel that will send the  
  2. // time with a period specified by the duration argument.  
  3. // It adjusts the intervals or drops ticks to make up for slow receivers.  
  4. // ...  
  5. func NewTicker(d Duration) *Ticker {  
  6.     ...  

signal.Notify

  1. // Notify causes package signal to relay incoming signals to c.  
  2. // ...  
  3. // Package signal will not block sending to c  
  4. // ...  
  5.    
  6. func Notify(c chan<- os.Signal, sig ...os.Signal) { 

ssh.Conn.OpenChannel

  1. // OpenChannel tries to open an channel.  
  2. // ...  
  3. // On success it returns the SSH Channel and a Go channel for  
  4. // incoming, out-of-band requests. The Go channel must be serviced, or  
  5. // the connection will hang.  
  6.    
  7. OpenChannel(name string, data []byte) (Channel, <-chan *Request, error) 

當(dāng)一個(gè)API向一個(gè)管道發(fā)送無界數(shù)據(jù)流時(shí),在實(shí)現(xiàn)API時(shí)面臨的問題是如果向管道發(fā)送數(shù)據(jù)會(huì)阻塞怎么辦。阻塞的原因可能是管道已經(jīng)滿了或者管道是無緩沖的,沒有g(shù)o協(xié)程準(zhǔn)備好接收數(shù)據(jù)。針對(duì)不同的場(chǎng)景要選擇合適的行為,但是每個(gè)場(chǎng)景必須作出選擇。例如,ssh包選擇了阻塞,并且文檔寫明如果你不接受數(shù)據(jù),連接就會(huì)被卡住。signal.Notify 和 time.Tick選擇不阻塞,直接丟棄數(shù)據(jù)。

不足的是,Go本身并沒有從類型或函數(shù)簽名角度提供方法指定默認(rèn)行為。作為API的設(shè)計(jì)者,你必須在文檔中寫明行為,不然其行為就是不定的。然而,多數(shù)情況下我們都是API的使用者而不是設(shè)計(jì)者,所以我們可以反過來記這個(gè)原則,反過來就是一條警告信息:

對(duì)于通過一個(gè)管道向一個(gè)慢速的消費(fèi)者發(fā)送無界數(shù)據(jù)的API,在沒有通讀API的文檔或者實(shí)現(xiàn)源碼之前,你不能確定API的行為。

原則 #3

向一個(gè)管道發(fā)送有界數(shù)據(jù),同時(shí)這個(gè)管道是作為參數(shù)傳遞進(jìn)來的API,必須用文檔寫明對(duì)于慢速消費(fèi)者的行為。

不好的例子

rpc.Client.Go

  1. func (client *Client) Go(serviceMethod string,  
  2.                          args interface{},  
  3.                          reply interface{},  
  4.                          done chan *Call  
  5.                          ) *Call 

這個(gè)原則和第二個(gè)原則類似,不同點(diǎn)在于這個(gè)原則用于發(fā)送有界數(shù)據(jù)的API。不幸的是,在標(biāo)準(zhǔn)庫中沒有很好的例子。標(biāo)準(zhǔn)庫中唯一的API就是rpc.Client.Go,但它違背了我們的原則。文檔上這么寫:

Go異步的調(diào)用這個(gè)函數(shù)。它會(huì)返回代表著調(diào)用的Call數(shù)據(jù)結(jié)構(gòu)。在調(diào)用完成時(shí),done管道會(huì)通過返回同一個(gè)Call對(duì)象來觸發(fā)。如果done是空的,Go會(huì)分配一個(gè)新的管道;如果不空,done必須是有緩沖的,不然Go就會(huì)崩潰。

Go發(fā)送了有界數(shù)據(jù)(只有1,當(dāng)遠(yuǎn)程調(diào)用結(jié)束時(shí))。但是注意到,由于管道是被當(dāng)作參數(shù)傳遞到函數(shù)中的,所以它仍然存在慢速消費(fèi)者問題。即使你必須傳一個(gè)帶緩沖的管道進(jìn)來,如果管道已滿,向這個(gè)管道發(fā)送數(shù)據(jù)仍然可能會(huì)阻塞。文檔并沒有定義這種場(chǎng)景下的行為。需要我們來讀讀源碼了:

src/pkg/net/rpc/client.go

  1. func (call *Call) done() {  
  2.      select {  
  3.      case call.Done <- call:  
  4.      // ok  
  5.      default:  
  6.      // We don't want to block here.  It is the caller's responsibility to make  
  7.          // sure the channel has enough buffer space. See comment in Go().  
  8.          if debugLog {  
  9.              log.Println("rpc: discarding Call reply due to insufficient Done chan capacity")  
  10.          }  
  11.      }  

噢!如果done管道沒有合適的緩沖,RPC的響應(yīng)可能丟失了。

原則 #4

向一個(gè)管道發(fā)送無界數(shù)據(jù)流的API應(yīng)該接受管道作為參數(shù),而不是返回一個(gè)新的管道。

例子

signal.Notify

  1. func Notify(c chan<- os.Signal, sig ...os.Signal) 

ssh.NewClient

  1. func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client 

當(dāng)我第一次看到signal.Notify這個(gè)API時(shí),我很疑惑,“為什么它接收一個(gè)管道作為輸入而不是直接返回一個(gè)管道給我用?”“使用這個(gè)API需要調(diào)用方分配一個(gè)管道,難道API就不能替我們做么,像下面這樣?”

  1. func Notify(sig ...os.Signal) <-chan os.Signal 

文檔幫助我們理解為什么這不是好的選擇:

signal包向c發(fā)送數(shù)據(jù)時(shí)并不會(huì)阻塞:調(diào)用方必須保證c有足夠的緩沖空間來跟得上潛在的信號(hào)速度

signal.Notify接收管道作為參數(shù),因?yàn)樗丫彌_空間的控制權(quán)交給了調(diào)用方。這使得調(diào)用方可以選擇,在處理一個(gè)信號(hào)時(shí),可以安全的忽略多少信號(hào),這需要和緩存這些信號(hào)的內(nèi)存開銷作折衷考慮。

緩沖大小的控制在高吞吐系統(tǒng)中尤為重要。設(shè)想一個(gè)高吞吐的發(fā)布訂閱系統(tǒng)的這樣一個(gè)接口:

  1. func Subscribe(topic string, msgs chan<- Msg) 

往管道中發(fā)送越多的消息,管道同步稱為性能瓶頸的可能性越大。由于API允許調(diào)用方創(chuàng)建管道,調(diào)用方需要考慮緩沖,進(jìn)而性能可以由調(diào)用方控制。這是一種更靈活的設(shè)計(jì)。

如果僅僅是控制緩沖的大小,我們可能會(huì)爭(zhēng)論如下的API就足夠了:

  1. func Notify(sig ...os.Signal, bufSize int) <-chan os.Signal 

這樣設(shè)計(jì),管道作為參數(shù)還是必須的,因?yàn)檫@樣允許調(diào)用方使用一個(gè)管道動(dòng)態(tài)的處理不同類型的信號(hào)。這樣設(shè)計(jì)為調(diào)用方提供了更多的程序結(jié)構(gòu)和性能上的靈活性。作為一個(gè)假想實(shí)驗(yàn),讓我們用Subscribe API來構(gòu)建需求。訂閱newcustomer管道,并對(duì)于每一條消息,為消費(fèi)者訂閱其主題。如果API允許我們傳遞接收管道,我們可以這樣寫:

  1. msgs := make(chan Msg, 128)  
  2.    
  3. Subscribe("newcustomer", msgs)  
  4. for m := range msgs {  
  5.     switch m.Topic {  
  6.     case "newcustomer":  
  7.         Subscribe(msg.Payload, msgs)  
  8.     default:  
  9.         handleCustomerMessage(m)  

但是,如果管道被返回了,調(diào)用方不得不為每一個(gè)訂閱啟動(dòng)一個(gè)單獨(dú)的go協(xié)程。這在任何復(fù)用場(chǎng)景都會(huì)帶來額外的內(nèi)存和同步開銷:

  1. for m := range Subscribe("newcustomer") {  
  2.     go subCustomer(m.Payload)  
  3. }  
  4.    
  5. func subCustomer(topic string) {  
  6.     for m := range Subscribe(topic) {  
  7.         handleCustomerMessage(m)  
  8.     }  

原則 #5

發(fā)送有界數(shù)據(jù)的API可以通過返回一個(gè)合適大小緩沖的管道來達(dá)到目的。

例子:

http.CloseNotifier

  1. type CloseNotifier interface {  
  2.         // CloseNotify returns a channel that receives a single value  
  3.         // when the client connection has gone away.  
  4.         CloseNotify() <-chan bool  

time.After

  1. func After(d Duration) <-chan Time 

當(dāng)API向一個(gè)管道發(fā)送有界數(shù)據(jù)時(shí),可以返回一個(gè)擁有容納全部數(shù)據(jù)的緩沖空間的管道。這個(gè)要返回的管道的方向性標(biāo)識(shí)保證了調(diào)用方必須遵守約定。CloseNotify 和After返回的管道 都利用了這一點(diǎn)。

同時(shí),需要注意到,通過允許調(diào)用方傳遞一個(gè)管道來接收數(shù)據(jù),這些調(diào)用可能會(huì)更靈活。但需要處理當(dāng)管道滿了的時(shí)候(原則3)。例如,另外一個(gè)可選的,更靈活的CloseNotifier:

  1. type CloseNotifier interface {  
  2.         // CloseNotify sends a single value with the ResponseWriter whose  
  3.         // underlying connection has gone away.  
  4.         CloseNotify(chan<- http.ResponseWriter)  

但是這種額外的靈活性帶來的開銷并不值得關(guān)注,因?yàn)閱我坏恼{(diào)用方很少會(huì)同時(shí)等待多個(gè)關(guān)閉通知。畢竟,關(guān)閉通知只有在某個(gè)連接上下文內(nèi)才有效。不同的連接一般都是相互獨(dú)立的。

特例

一些API打破了我們的原則,需要仔細(xì)分析。

原則 #1 的特例

API需要聲明管的方向性。

例子

rpc.Client.Go

傳過來的done管道沒有方向性標(biāo)識(shí)符:

  1. func (client *Client) Go(serviceMethod string,  
  2.                          args interface{},  
  3.                          reply interface{},  
  4.                          done chan *Call  
  5.                          ) *Call 

直觀上看,這樣做是因?yàn)閐one管道是作為Call結(jié)構(gòu)體的一部分返回的。

  1. type Call struct {  
  2.         // ...  
  3.         Done          chan *Call  // Strobes when call is complete.  

這種靈活性是需要的,這樣允許在你傳nil時(shí)分配一個(gè)done管道出來。如果堅(jiān)持原則1,就需要從Call結(jié)構(gòu)中去除done并且聲明兩個(gè)函數(shù):

  1. func (c *Client) Go(method string,  
  2.                     args interface{},  
  3.                     reply interface{}  
  4.                     ) (*Call, <-chan *Call)  
  5.    
  6. func (c *Client) GoEx(method string,  
  7.                       args interface{},  
  8.                       reply interface{},  
  9.                       done chan<- *Call  
  10.                       ) *Call 

原則 #4 的特例

向管道發(fā)送無界數(shù)據(jù)流的API需要接收管道作為參數(shù),而不是返回一個(gè)新的管道。

例子

go.crypto/ssh

  1. func NewClientConn(c net.Conn, addr string, config *ClientConfig)  
  2.     (Conn, <-chan NewChannel, <-chan *Request, error) 

time.Tick

  1. func Tick(d Duration) <-chan Time 

go.crypto/ssh包幾乎在所有的地方都返回了無界的數(shù)據(jù)流管道。ssh.NewClientConn只是其中的一個(gè)。給調(diào)用者更多控制權(quán)和靈活性的API應(yīng)該是這樣:

  1. func NewClientConn(c net.Conn,  
  2.                    addr string,  
  3.                    config *ClientConfig,  
  4.                    channels chan<- NewChannel,  
  5.                    reqs chan<- *Request  
  6.                    ) (Conn, error) 

time.Tick也違反了這個(gè)原則,但是易于理解。我們很少會(huì)創(chuàng)建非常多的計(jì)時(shí)器,通常都是獨(dú)立的處理不同的計(jì)時(shí)器。這個(gè)例子中緩沖也沒太大意義。

第二部分:那些原本可能使用的管道

這篇文章是一篇長文,所以我準(zhǔn)備分成兩部分講。接下來會(huì)提很多問題,為什么標(biāo)準(zhǔn)庫中可以使用管的地方卻沒有用管道。例如,http.Serve 返回了一個(gè)永不結(jié)束的等待被處理的請(qǐng)求流,為什么用了回調(diào)函數(shù)而不是將這些請(qǐng)求發(fā)送到一個(gè)處理管道中?第二部分會(huì)介紹更多!

原文鏈接: Alan Shreve   翻譯: 伯樂在線 - Codefor

譯文鏈接: http://blog.jobbole.com/73700/

責(zé)任編輯:林師授 來源: 伯樂在線
相關(guān)推薦

2017-06-19 14:21:01

JavaScriptAPI設(shè)計(jì)原則

2016-03-29 09:59:11

JavaScriptAPI設(shè)計(jì)

2022-02-10 23:38:23

API架構(gòu)設(shè)計(jì)

2023-09-21 11:20:46

2014-07-02 21:20:56

CA TechnoloAPI

2015-09-23 17:12:18

API設(shè)計(jì)原則

2024-08-26 15:35:40

2015-09-24 08:52:53

API設(shè)計(jì)原則

2025-03-27 00:45:00

2024-09-19 08:46:46

SPIAPI接口

2010-10-11 11:25:26

MySQL主鍵

2022-09-27 09:21:34

SOLID開閉原則Go

2020-01-08 14:45:38

Kubernetes存儲(chǔ)架構(gòu)

2024-03-13 15:21:24

APIJava原則

2010-10-19 17:21:35

SQL SERVER主

2012-05-08 10:14:45

設(shè)計(jì)原則

2013-04-17 10:46:54

面向?qū)ο?/a>

2014-04-25 10:13:00

Go語言并發(fā)模式

2010-01-28 10:01:28

C++的設(shè)計(jì)原則

2014-09-10 10:35:11

Material De設(shè)計(jì)原則
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)