GOLANG介面適配,組合方式的靈活介面演化

winlin發表於2017-05-15

在OO(Object Oriented)原則中,有一條叫做:優先使用組合,而不是繼承。雖然GOLANG並不是OO的語言(沒有繼承和多型),但是不妨礙GOLANG使用這條原則,而GOLANG的作者就強調過這一點,在GOLANG中是使用組合而非繼承來擴充套件。

裝逼的說來,繼承是一種名詞化的語言體系,先進行業務抽象然後設計類體系和繼承關係。而組合,強制使用介面,因為組合中使用的總是另外一個物件的介面,通過動詞的組合,實現目標,比如不管是什麼只要有Write([]byte)(int,error)這個動作,就實現了這個介面,其他物件組合這個介面後,對外也看起來就是個io.Writer的介面。

比如,GOALNG1.8支援了writev,一般在物件導向會這麼的搞:

class Socket {
int Write(void*, int);
int Writev(const iovec*, int);
};

對的吧?一個Socket可以寫資料,也可以用writev寫iovec向量,就是一次性寫入多個記憶體塊。

Note: 有時候記憶體塊是不連續的,比如一個Video幀,傳送給不同的客戶端時,Header是需要修改的,但是Payload都一樣,那麼可以針對每個客戶端只建立一個header,然後公用payload,但是這時候兩個記憶體指標是不連續的,特別是需要同時寫入多個視訊幀時,writev就很神奇的避免了記憶體拷貝writev(header+payload),具體參考下writev的資料哈。

這樣有個問題,並非所有系統都支援Writev的,並非所有Socket都支援Writev的,如果是自己寫個程式碼,當然是可以隨便這麼搞的,但是作為標準庫,GOLANG當然是不能這麼做的。GOLANG就加了一個介面(一個新動作)叫做net.buffersWriter,如果實現了這個介面就用writev。先看用法:

    conn,err := net.Dial("tcp", "127.0.0.1:1935")

    buffers := Buffers{
        []byte("once upon a time in "),
        []byte("Gopherland ... "),
    }

    buffers.WriteTo(conn)

在Buffers的WriteTo方法會判斷是否是writev的介面,如果是則用writev寫,否則就一個個的寫:

func (v *Buffers) WriteTo(w io.Writer) (n int64, err error) {
    if wv, ok := w.(buffersWriter); ok {
        return wv.writeBuffers(v)
    }

實際上conn是net.TcpConn,裡面有個fd *net.netFD,它實現了net.buffersWriter介面,所以最後呼叫的就是(fd *netFD) writeBuffers(v *Buffers)

func (c *conn) writeBuffers(v *Buffers) (int64, error) {
    n, err := c.fd.writeBuffers(v)

func (fd *netFD) writeBuffers(v *Buffers) (n int64, err error) {
        iovecs = append(iovecs, syscall.Iovec{Base: &chunk[0]})
        wrote, _, e0 := syscall.Syscall(syscall.SYS_WRITEV,
            uintptr(fd.sysfd),
            uintptr(unsafe.Pointer(&iovecs[0])),
            uintptr(len(iovecs)))

對於其他沒有實現這個介面的物件,就每個向量迴圈的寫。

在看一個例子http.Get(url string),客戶端發起一個HTTP請求:

http.Get("http://localhost:1985/api/v1/versions")
// 實際上呼叫的是:
func (c *Client) Get(url string)
// 然後呼叫:
(c *Client) Do(req *Request)

在GOLANG1.7中引入了context的概念,用來支援cancel,怎麼用的:

ctx,cancel := context.WithCancel(context.Background())

select {
case <- ctx.Done():
    // Cancelled.
case <- time.After(...):
    // Timeout
case <- other events:
    // Other events.
}

如何支援取消的HTTP請求呢?給http.Get加個ctx引數?例如http.Get(ctx, url)這樣?那改動得多大啊,而且還不能相容之前的API,淚奔~看看GOLANG的解決:

ctx,cancel := context.WithCancel(context.Background())
go func(){
    req,err := http.NewRequest("http://...")
    res,err := http.DefaultClient.Do(req.WithContext(ctx))
    defer res.Body.Close()
    // 讀取res響應結果。
}()

select {
case <- ctx.Done():
case <- time.After(3 * time.Second):
    cancel() // Timeout to cancel all requests.
}

使用組合,通過req.WithContext再返回一個*http.Request,實現同樣的目的。

相關文章