Go教程:21-io.Reader/Writer详解
I/O操作也叫输入输出操作.其中I是指Input,O是指Output,用于读或者写数据的,有些语言中也叫流操作,是指数据通信的通道. Golang 标准库对 IO 的抽象非常精巧,各个组件可以随意组合,可以作为接口设计的典范.
Go原生的pkg中有一些核心的interface,其中io.Reader/Writer是比较常用的接口. Go Writer 和 Reader接口的设计遵循了Unix的输入和输出,一个程序的输出可以是另外一个程序的输入.
1. io.Reader/Writer
很多原生的结构都围绕这个系列的接口展开,在实际的开发过程中,您会发现通过这个接口可以在多种不同的io类型之间进行过渡和转化. io.Reader 和 io.Writer 接口定义如下:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
1.1 io.Reader/Writer,有几个常用的实现:
- net.Conn: 网络
- os.Stdin, os.Stdout, os.Stderr: console终端标准输出,err
- os.File: 网络,标准输入输出,文件的流读取
- strings.Reader: 把字符串抽象成Reader
- bytes.Reader: 把[]byte抽象成Reader
- bytes.Buffer: 把[]byte抽象成Reader和Writer
- bufio.Reader/Writer: 抽象成带缓冲的流读取(比如按行读写)
2. io.Reader/Writer使用场景
Unix 下有一切皆文件的思想,Golang 把这个思想贯彻到更远,因为本质上我们对文件的抽象就是一个可读可写的一个对象, 也就是实现了io.Writer 和 io.Reader 的对象我们都可以称为文件,
2.1 文件写入
类型 os.File 表示本地系统上的文件.它实现了 io.Reader 和 io.Writer ,因此可以在任何 io 上下文中使用. 例如,下面的例子展示如何将连续的字符串切片直接写入文件:
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")
}
2.2 Golang HTTP 下载文件
http.Response.Body 实现了io.ReadCloser接口,也实现了io.Reader协议. os.File实现了io.Writer, 通过io.Copy()直接使用copy http.Response.Body 到 os.File,我们将数据流传输到文件中,避免将其全部加载到内存中.
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)
if err != nil {
return err
}
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
return err
}
2.3 Golang实现简单HTTP Proxy
使用HTTP/1.1协议中的CONNECT方法建立起来的隧道连接,实现的HTTP Proxy. 这种代理的好处就是不用知道客户端请求的数据,只需要原封不动的转发就可以了,对于处理HTTPS的请求就非常方便了,不用解析他的内容,就可以实现代理.
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)
}