1. I/O操作
I/O操作也叫输入输出操作。其中I是指Input,O是指Output,用于读或者写数据的,有些语言中也叫流操作,是指数据通信的通道。
Golang 标准库对 IO 的抽象非常精巧,各个组件可以随意组合,可以作为接口设计的典范。
Go原生的pkg中有一些核心的interface,其中io.Reader/Writer是比较常用的接口。
Go Writer 和 Reader接口的设计遵循了Unix的输入和输出,一个程序的输出可以是另外一个程序的输入。
![io.Reader/Writer详解](/Users/luozhibo/项目资料/Blizz 项目测试/images/c2dtZl8vaW1nL2JWYmR6amE_dz0xNjAwJmg9MjE0-1429897.jpg)
2. io.Reader/Writer
很多原生的结构都围绕这个系列的接口展开,在实际的开发过程中,您会发现通过这个接口可以在多种不同的io类型之间进行过渡和转化.。
io.Reader 和 io.Writer 接口定义如下:
1
2
3
4
5
6
7
|
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
|
3. 总览
![image-20200927101833831](/Users/luozhibo/项目资料/Blizz 项目测试/images/image-20200927101833831-1429897.png)
围绕io.Reader/Writer,有几个常用的实现:
- net.Conn, os.Stdin, os.File: 网络、标准输入输出、文件的流读取
- strings.Reader: 把字符串抽象成Reader
- bytes.Reader: 把[]byte抽象成Reader
- bytes.Buffer: 把[]byte抽象成Reader和Writer
- bufio.Reader/Writer: 抽象成带缓冲的流读取(比如按行读写)
这些实现对于初学者来说其实比较难去记忆,在遇到实际问题的时候更是一脸蒙圈,不知如何是好。
4. io.Reader/Writer使用场景
Unix 下有一切皆文件的思想,Golang 把这个思想贯彻到更远,因为本质上我们对文件的抽象就是一个可读可写的一个对象,也就是实现了io.Writer 和 io.Reader 的对象我们都可以称为文件。
4.1 文件写入
类型 os.File 表示本地系统上的文件。它实现了 io.Reader 和 io.Writer ,因此可以在任何 io 上下文中使用。
例如,下面的例子展示如何将连续的字符串切片直接写入文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
func main() {
proverbs := []string{
"tech.mojotv.cn\n",
"code.mojotv.cn\n",
"github.com/libragen\n",
"rocks my world\n",
}
file, err := os.Create("./fileMojotvIO.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
for _, p := range proverbs {
// file 类型实现了 io.Writer
n, err := file.Write([]byte(p))
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if n != len(p) {
fmt.Println("failed to write bytes")
os.Exit(1)
}
}
fmt.Println("file write finished")
}
|
4.2 Golang HTTP 下载文件
http.Response.Body 实现了io.ReadCloser接口,也实现了io.Reader协议。
os.File实现了io.Writer,通过io.Copy()直接使用copy http.Response.Body 到 os.File,我们将数据流传输到文件中,避免将其全部加载到内存中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
package main
import (
"io"
"net/http"
"os"
)
func main() {
fileUrl := "https://mojotv.cn/assets/image/logo01.png"
if err := DownloadFile("avatar.jpg", fileUrl); err != nil {
panic(err)
}
}
// DownloadFile will download a url to a local file. It's efficient because it will
// write as it downloads and not load the whole file into memory.
func DownloadFile(filepath string, url string) error {
// Get the data
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Create the file
out, err := os.Create(filepath) // os.File实现了io.Writer
if err != nil {
return err
}
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body) // http.Response.Body 实现了io.ReadCloser接口,也实现了io.Reader协议。
return err
}
|
4.3 Golang实现简单HTTP Proxy
使用HTTP/1.1协议中的CONNECT方法建立起来的隧道连接,实现的HTTP Proxy。
这种代理的好处就是不用知道客户端请求的数据,只需要原封不动的转发就可以了,对于处理HTTPS的请求就非常方便了,不用解析他的内容,就可以实现代理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
package main
import (
"bytes"
"fmt"
"io"
"log"
"net"
"net/url"
"strings"
)
func main() {
//设置日志格式
log.SetFlags(log.LstdFlags|log.Lshortfile)
//监听端口和地址
l, err := net.Listen("tcp", ":8081")
if err != nil {
log.Panic(err)
}
for {
client, err := l.Accept()
if err != nil {
log.Panic(err)
}
//Listener接口的Accept方法,会接受客户端发来的连接数据,这是一个阻塞型的方法,如果客户端没有连接数据发来,
// 他就是阻塞等待.接收来的连接数据,会马上交给handleClientRequest方法进行处理,
// 这里使用一个go关键字开一个goroutine的目的是不阻塞客户端的接收,代理服务器可以马上接收下一个连接请求.
go handleClientRequest(client)
}
}
func handleClientRequest(client net.Conn) {
if client == nil {
return
}
defer client.Close()
var b [1024]byte
n, err := client.Read(b[:])
if err != nil {
log.Println(err)
return
}
var method, host, address string
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &host)
hostPortURL, err := url.Parse(host)
if err != nil {
log.Println(err)
return
}
if hostPortURL.Opaque == "443" { //https访问
address = hostPortURL.Scheme + ":443"
} else { //http访问
if strings.Index(hostPortURL.Host, ":") == -1 { //host不带端口, 默认80
address = hostPortURL.Host + ":80"
} else {
address = hostPortURL.Host
}
}
//获得了请求的host和port,就开始拨号吧
server, err := net.Dial("tcp", address)
if err != nil {
log.Println(err)
return
}
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")
} else {
server.Write(b[:n])
}
//进行转发
go io.Copy(server, client)
io.Copy(client, server)
}
|
4.4 base64编码成字符串(使用了Writer)
encoding/base64包中:
|
func NewEncoder(enc *Encoding, w io.Writer) io.WriteCloser
|
这个用来做base64编码,但是仔细观察发现,它需要一个io.Writer作为输出目标,并用返回的WriteCloser的Write方法将结果写入目标,下面是Go官方文档的例子
1
2
3
|
input := []byte("foo\x00bar")
encoder := base64.NewEncoder(base64.StdEncoding, os.Stdout)
encoder.Write(input)
|
这个例子是将结果写入到Stdout。
如果我们希望得到一个字符串呢?观察上面的图,不然发现可以用bytes.Buffer作为目标io.Writer:
1
2
3
4
5
|
input := []byte("foo\x00bar")
buffer := new(bytes.Buffer)
encoder := base64.NewEncoder(base64.StdEncoding, buffer)
encoder.Write(input)
fmt.Println(string(buffer.Bytes())
|
0x02 []byte和struct之间正反序列化
这种场景经常用在基于字节的协议上,比如有一个具有固定长度的结构:
1
2
3
4
5
6
7
|
type Protocol struct {
Version uint8
BodyLen uint16
Reserved [2]byte
Unit uint8
Value uint32
}
|
通过一个[]byte来反序列化得到这个Protocol,一种思路是遍历这个[]byte,然后逐一赋值。其实在encoding/binary包中有个方便的方法:
|
func Read(r io.Reader, order ByteOrder, data interface{}) error
|
这个方法从一个io.Reader中读取字节,并已order指定的端模式,来给填充data(data需要是fixed-sized的结构或者类型)。要用到这个方法首先要有一个io.Reader,从上面的图中不难发现,我们可以这么写:
1
2
3
4
|
var p Protocol
var bin []byte
//...
binary.Read(bytes.NewReader(bin), binary.LittleEndian, &p)
|
换句话说,我们将一个[]byte转成了一个io.Reader。
反过来,我们需要将Protocol序列化得到[]byte,使用encoding/binary包中有个对应的Write方法:
|
func Write(w io.Writer, order ByteOrder, data interface{}) error
|
通过将[]byte转成一个io.Writer即可:
1
2
3
4
5
|
var p Protocol
buffer := new(bytes.Buffer)
//...
binary.Writer(buffer, binary.LittleEndian, p)
bin := buffer.Bytes()
|
4.5 从流中按行读取
比如对于常见的基于文本行的HTTP协议的读取,我们需要将一个流按照行来读取。
本质上,我们需要一个基于缓冲的读写机制(读一些到缓冲,然后遍历缓冲中我们关心的字节或字符)。
在Go中有一个bufio的包可以实现带缓冲的读写:
1
2
|
func NewReader(rd io.Reader) *Reader
func (b *Reader) ReadString(delim byte) (string, error)
|
这个ReadString方法从io.Reader中读取字符串,直到delim,就返回delim和之前的字符串。
如果将delim设置为\n,相当于按行来读取了:
1
2
3
4
5
6
7
|
var conn net.Conn
//...
reader := NewReader(conn)
for {
line, err := reader.ReadString([]byte('\n'))
//...
}
|
5. 技巧
5.1 string转[]byte
1
2
|
a := "Hello, playground"
fmt.Println([]byte(a))
|
等价于
1
2
3
4
|
a := "Hello, playground"
buf := new(bytes.Buffer)
buf.ReadFrom(strings.NewReader(a))
fmt.Println(buf.Bytes())
|
源文地址:http://www.manoner.com/post/GoLand/Go%E4%B8%ADReader%E5%92%8CWriter%E8%AF%A6%E8%A7%A3/
|