使用go的io.Pipe优雅的优化中间缓存

3

BEFORE

今天发现,go的优势除了它的轻量线程(goroutine)提供了更方便灵活的并发编程模式之外,它的I/O机制也设计的非常给力。

之前,我在向其他服务器发送json数据时,都需要先声明一个bytes缓存,然后通过json库把结构体中的内容mashal成字节流,再通过Post函数发送。
代码如下:

package main

import (
        "bytes"
        "encoding/json"
        "io/ioutil"
        "log"
        "net/http"
)

func init() {
        log.SetFlags(log.Lshortfile)
}

func main() {
        cli := http.Client{}

        msg := struct {
            Name, Addr string
            Price      float64
        }{
            Name:  "hello",
            Addr:  "beijing",
            Price: 123.56,
        }
        buf := bytes.NewBuffer(nil)
        json.NewEncoder(buf).Encode(msg)
        resp, err := cli.Post("http://localhost:9999/json", "application/json", buf)
        
        if err != nil {
            log.Fatalln(err)
        }

        body := resp.Body
        defer body.Close()

        if body_bytes, err := ioutil.ReadAll(body); err == nil {
            log.Println("response:", string(body_bytes))
        } else {
            log.Fatalln(err)
        }
}

这样就总是需要我们在内存中预先分配一个缓存,大部分情况下其实还好,BUT 如果需要发送的数据很大,就会严重影响性能了。

AFTER

go设计了一个Pipe函数帮我们解决这个问题,于是我的代码可以改为:

package main

import (
        "encoding/json"
        "io"
        "io/ioutil"
        "log"
        "net/http"
        "time"
)

func init() {
        log.SetFlags(log.Lshortfile)
}
func main() {
        cli := http.Client{}

        msg := struct {
            Name, Addr string
            Price      float64
        }{
            Name:  "hello",
            Addr:  "beijing",
            Price: 123.56,
        }
        r, w := io.Pipe()
        // 注意这边的逻辑!!
        go func() {
            defer func() {
                time.Sleep(time.Second * 2)
                log.Println("encode完成")
                // 只有这里关闭了,Post方法才会返回
                w.Close()
            }()
            log.Println("管道准备输出")
            // 只有Post开始读取数据,这里才开始encode,并传输
            err := json.NewEncoder(w).Encode(msg)
            log.Println("管道输出数据完毕")
            if err != nil {
                log.Fatalln("encode json failed:", err)
            }
        }()
        time.Sleep(time.Second * 1)
        log.Println("开始从管道读取数据")
        resp, err := cli.Post("http://localhost:9999/json", "application/json", r)

        if err != nil {
            log.Fatalln(err)
        }
        log.Println("POST传输完成")

        body := resp.Body
        defer body.Close()

        if body_bytes, err := ioutil.ReadAll(body); err == nil {
            log.Println("response:", string(body_bytes))
        } else {
            log.Fatalln(err)
        }
}

输出如下:

main.go:35: 管道准备输出
main.go:44: 开始从管道读取数据
main.go:38: 管道输出数据完毕
main.go:31: encode完成
main.go:50: POST传输完成
main.go:56: response: {"Name":"hello","Addr":"beijing","Price":123.56}

可以看到,通过Pipe,我们可以方便的链接输入和输出,只要我们能正确理解整个过程的逻辑。有了它,我们终于可以甩去讨厌的中间缓存了,同时也提高了系统的稳定性。

服务器端代码

为了方便大家调试,以下是服务器端代码:

package main

import (
           "encoding/json"
           "io/ioutil"
           "log"
           "net/http"
)

func init() {
           log.SetFlags(log.Lshortfile)
}

func main() {
           http.HandleFunc("/json", handleJson)

           http.ListenAndServe(":9999", nil)
}

func handleJson(resp http.ResponseWriter, req *http.Request) {
           if req.Method == "POST" {
               body := req.Body
               defer body.Close()
               body_bytes, err := ioutil.ReadAll(body)
               if err != nil {
                   log.Println(err)
                   resp.Write([]byte(err.Error()))
                   return
               }
               j := map[string]interface{}{}
               if err := json.Unmarshal(body_bytes, &j); err != nil {
                   log.Println(err)
                   resp.Write([]byte(err.Error()))
                   return
               }
               resp.Write(body_bytes)
           } else {
               resp.Write([]byte("请使用post方法!"))
           }
}

你可能感兴趣的

11 条评论
flybywind 作者 · 2016年10月18日

调用w.Close,r就会收到一个EOF,这样Post才会终止。你可以在post完成后主动调用r.Close,不调用也没关系,随着程序结束,r会自动关闭。但是w必须手动调用,否则会死锁。

+1 回复

voidint · 2016年10月18日

r, w := io.Pipe()里面的r怎么没有close?

回复

voidint · 2016年10月18日

拿你代码跑了下,客户端不主动调用w.Close(),服务端就会在ioutil.ReadAll()上卡死,ioutil.ReadAll()读取不到EOF或者error就不会返回。

回复

flybywind 作者 · 2016年10月18日

不可能啊,我都是跑通的。而且go线程怎么可能执行不到Close呢。

回复

voidint · 2016年10月18日

上面的代码能跑通,我只是试了下注释掉w.Close()的情况。

回复

flybywind 作者 · 2016年10月18日

你没看到我开始给你回复的吗,注释掉肯定会死锁啊!

回复

voidint · 2016年10月18日

怎么就不理解我意思呢,哥们?看到新奇点的代码,跑一跑,改一改,才能有新收获啊。你还不允许别人折腾下,自由发挥下?

回复

dotcoo_zhao · 2016年11月06日

简单问题复杂化

回复

linkerlin · 2018年03月23日

Pipe主要用在 比较重的操作,性能开销比buffer大多了.

回复

wujohns · 2018年03月30日

挺好的,只是评论中大多人都没遇到有这样需求的场景,实际要用到这个特性的场景还是挺多的

回复

苹果的梨 · 4月8日

用sync.pool怎么样呢?

回复

载入中...