Go中输入和输出操作都采用原语来实现,原语会将数据模拟成可读的或可写的字节流。为方便开发使用Go将I/O操作封装在不同包中,而io
包是I/O原语(I/O Primitives)提供的基本接口。
读写库 | 描述 |
---|---|
io | 底层接口定义库,定义基本接口和常量。 |
io/ioutil | 包含在io目录下,作为io库的工具包,封装了实用的函数。 |
bufio | io库的添加缓存后的封装,实现带缓冲的I/O。 |
fmt | 实现了格式化I/O,类似C语言中的printf 和scanf 。 |
os | 与操作系统打交道 |
常用实现
Go中很多原生的结构都围绕io.Reader
和io.Writer
两个接口展开,通过这两个接口可以在多种不同的I/O之间进行过渡和转化。任何实现了Read()
函数的对象都可以作为io.Reader
,任何实现了Write()
函数的对象都可以作为io.Writer
。
io.Reader
和io.Writer
常用实现
常用实现 | 描述 |
---|---|
net.Conn | 网络 |
os.Stdin | 标准输入输出 |
os.File | 文件流读取 |
strings.Reader | 将字符串抽象成为读取器 |
bytes.Reader | 将字节数组抽象成读取器 |
bytes.Buffer | 将字节数组抽象成读写器 |
bufio.Reader/bufio.Writer | 抽象带缓冲的流读写器 |
基础接口
Go中输入和输出操作使用原语实现,原语将数据模拟称为可读或可写的字节流。
为此,io
包提供了对I/O原语的基本接口,包装了原语的已有实现,使之成为共享的公共接口,抽象出泛用的函数并附加了相关的原语操作。这些接口和原语是对底层实现完全不同的低水平操作的包装,除非得到其它方面的通知,客户端不应该假设它们是并发执行安全的。
io
包提供了最重要的两个接口io.Reader
和io.Writer
,分别用于数据的输入和输出。
接口 | 名称 | 描述 |
---|---|---|
io.Reader | 读取器 | 将数据从某个资源读取到传输缓冲区,被流式传输和使用。 |
io.Writer | 写入器 | 从缓冲区读取数据后写入到目标资源 |
io.Reader
io.Reader
表示一个读取器,将数据从某个资源读取到传输缓冲区。在缓冲区中,数据可以被流式传输和使用。
io.Reader
接口可用于包装基本的读取方法,利用io.Reader
可实现流式数据传输。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
对于要用作读取器的类型,必须实现io.Reader
接口的唯一方法Read(p []byte)
。只要实现了Read(p []byte)
它就是一个读取器。
Read(p []byte) (n int, err error)
Read()
入参p
是一个字节切片[]byte
,当数据读取时会写入该切片中,返回类型中整型n
表示读取到的字节数量,err
错误则表示读取过程中遇到的任何异常。当资源内容读取完毕,err
会为io.EOF
表示读取内容结束。
返回值 | 类型 | 描述 |
---|---|---|
n | int | 读取到的字节数 |
err | error | 发生错误时的信息,资源读取完毕会返回io.EOF 错误。 |
Read()
会将长度len(p)
个字节读取到p
变量中,返回读取的字节数量n
和错误err
。
读取机制
首先由于读取的字节数量n
的取值范围是0 <= n <= len(p)
,因此要尽可能读取。
Read()
读取过程中n < len(p)
会在调用过程中使用len(p)
作为暂存空间(缓冲区),若读取完毕n < len(p)
则会返回可用数据n
,而不会等待更多数据。
var str string
var buf []byte
var n int
var err error
var reader io.Reader
str = "hello world"
reader = strings.NewReader(str)
buf = make([]byte, 32)
n, err = reader.Read(buf)
fmt.Printf("%d, %+v, %s\n", n, err, buf) // 11, , hello world
buf = make([]byte, 32)
n, err = reader.Read(buf)
fmt.Printf("%d, %+v, %s\n", n, err, buf) // 0, EOF
当Read()
在成功读取n > 0
后遇到Error或EOF,本次调用结果一定会返回已读取到的n
,但可能返回的是(n, err)
, 或在下次调用中返回(0, err)
。
因此当Read()
返回Error时并不能代表没有读取到任何数据,调用者应该先处理返回的数据后,再处理可能的错误。
读取结果 | 描述 |
---|---|
(n, EOF) | 下次调用Read() 一定会返回(0, EOF) |
(n, nil) | 下次调用Read() 一定会返回(0, EOF) |
(0, nil) | 表示被阻塞,调用者应该将此种情况视为未进行操作。 |
例如:读取数据时突然关闭TCP套接字,根据情况可选择将字节保持在p
中或重试。
例如:将io.Reader
作为参数即可以从任意地方读取数据,比如从标准输入、文件、字符串等读取数据,只要来源实现io.Reader
接口。
func Read(reader io.Reader, size int) ([]byte, error) {
p := make([]byte, size)
n, err := reader.Read(p)
if n > 0 {
return p[:n], nil
}
return p, err
}
根据io.EOF
变量的定义var EOF = errors.New("EOF")
可知它是一个error
类型,因此当n>0
且数据被读完的情况下返回的error
可能是io.EOF
,也有可能是nil
。
bts, err := Read(os.Stdin, 11) //从标准输入读取
//bts,err := Read(strings.NewReader("hello world"), 12)//从字符串读取
fmt.Println(bts, err)
Read()
内部是被循环调用的,每次迭代,会从数据源读取一块数据放入缓冲区p
中,直到返回io.EOF
错误时停止。
例如:创建字符串读取器,流式地按字节读取。
str := "clear is better than clever"
//创建字符串读取器
reader := strings.NewReader(str)
//字节缓存区
buf := make([]byte, 4)
//循环读取
for{
//读取到缓存区
n,err := reader.Read(buf)
if err!=nil{
//读取结束
if err!=io.EOF{
fmt.Println("EOF:", n)
break
}
fmt.Println(err)
os.Exit(1)
}
result := string(buf[:n])
fmt.Println(n, result)
}
4 clea
4 r is
4 bet
4 ter
4 than
4 cle
3 ver
EOF
例如:实现从流中过滤掉非字母字符
//过滤函数
func alpha(c byte) byte{
if (c>='a'&&c<='z') || (c>='A'&&c<='Z'){
return c
}
return 0
}
type AlphaReader struct{
src string //资源
i int//当前读取的位置
}
func NewAlphaReader(src string) *AlphaReader{
return &AlphaReader{src:src}
}
func (r *AlphaReader) Read(p []byte) (int,error){
//判断是否已经读取到结尾
if r.i >= len(r.src){
return 0, io.EOF
}
//剩余未读取长度
x := len(r.src) - r.i
//缓存区填满大小
bound := 0
//判断剩余长度是否超过缓存区大小
if x >= len(p){
//完全填满缓冲区
bound = len(p)
}else if x < len(p){
//缓存区无法补满
bound = x
}
//创建缓存区
buf := make([]byte, bound)
//迭代读取
i := 0
for bound > i {
//每次读取一个字节
char := r.src[r.i]
//执行过滤函数
if c := alpha(char); c!=0{
buf[i] = c
}
i++
r.i++
}
//将缓存内容赋值到
copy(p, buf)
return i,nil
}
测试
str := "Hello! It's 9am, where is the sun?"
buf := make([]byte, 4)
reader := NewAlphaReader(str)
for {
n,err := reader.Read(buf)
if err==io.EOF{
break
}
data := string(buf[:n])
fmt.Print(data)
}
- 组合多个Reader可重用和屏蔽下层实现的复杂度
标准库已经实现了多个Reader,可使用一个Reader作为另一个Reader的实现是一种常见用法,可让一个Reader重用另一个Reader的逻辑。
例如:修改AlphaReader以接受io.Reader
作为其源
//过滤函数
func alpha(c byte) byte{
if (c>='a'&&c<='z') || (c>='A'&&c<='Z'){
return c
}
return 0
}
type AlphaReader struct{
reader io.Reader//组合标准读取器
}
func NewAlphaReader(reader io.Reader) *AlphaReader{
return &AlphaReader{reader:reader}
}
func (r *AlphaReader) Read(p []byte) (int,error){
n,err := r.reader.Read(p)
if err!=nil{
return n,err
}
buf := make([]byte, n)
for i:=0; i
测试
str := "Hello! It's 9am, where is the sun?"
reader := strings.NewReader(str)
buf := make([]byte, 4)
r := NewAlphaReader(reader)
for {
n,err := r.Read(buf)
if err==io.EOF{
break
}
data := string(buf[:n])
fmt.Print(data)
}
例如:与os.File
结束过滤掉文件中的非字母字符
$ vim ./test.txt
Hello! It's 9am, where is the sun?
file,err := os.Open("./test.txt")
if err!=nil{
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
r := NewAlphaReader(file)
buf := make([]byte, 4)
for {
n,err := r.Read(buf)
if err==io.EOF{
break
}
data := string(buf[:n])
fmt.Print(data)
}
io.Writer
-
io.Writer
表示一个编写器,从缓存区读取数据,并将数据写入目标资源。
- 对于要作为编写器的类型必须实现
io.Writer
接口唯一的方法Write(p []byte)
,只要实现了Write(p []byte)
就是一个编写器。
type Writer interface{
Write(p []byte) (n int, err error)
}
-
Write()
方法具有两个返回值,一个是写入到目标资源的字节数,一个是发生错误时的错误。
返回值 | 类型 | 描述 |
---|---|---|
n | int | 写入到目标资源的字节数 |
err | error | 发生错误时的错误 |
例如:使用bytes.Buffer
类型作为io.Writer
将数据写入内存缓存区
proverbs := []string{
"many hands make light work",
"measure for measure",
"murder will out",
"never say die",
}
var writer bytes.Buffer
for _,p := range proverbs{
n,err := writer.Write([]byte(p))
if err!=nil{
fmt.Println(err)
os.Exit(1)
}
if n != len(p){
fmt.Println("failed to write data")
os.Exit(1)
}
}
result := writer.String()
fmt.Println(result)
例如:实现自定义的io.Writer
将其内容作为字节序列写入通道
//ChanWriter 通道写入器
type ChanWriter struct{
ch chan byte//目标资源
}
//NewChanWriter 创建通道写入器
func NewChanWriter() *ChanWriter{
return &ChanWriter{ch:make(chan byte, 1024)}
}
//Chan 获取目标资源
func (w *ChanWriter) Chan() <-chan byte{
return w.ch
}
//Close 关闭目标资源
func (w *ChanWriter) Close(){
close(w.ch)
}
//Write 写入目标资源
func (w *ChanWriter) Write(p []byte) (int, error){
n := 0
//遍历输入数据按字节写入目标资源
for _,b := range p{
w.ch<-b
n++
}
return n,nil
}
测试
writer := NewChanWriter()
go func(){
defer writer.Close()
writer.Write([]byte("money can talk"))
}()
for char := range writer.Chan(){
fmt.Printf("%c\n", char)
}
io.EOF
- EOF是
End-Of-File
的缩写,根据Go惯例大写字母缩写表示常量,不过io.EOF
被定义成了变量。 -
io.EOF
是io
包中的一个变量,表示文件结束的错误。
1package io23var EOF = errors.New("EOF")
-
io.EOF
是Go中最重要的错误变量,用于表示输入流的结尾,因为每个文件都有一个结尾,所以io.EOF
很多时候并不能算是一个错误,更重要是表示输入流结束了。
var EOF = errors.New("EOF")
io.Closer
-
Closer
是包装基本Close()
方法的接口用于关闭数据流,第一次调用后Closer
的行为是未定义的。 - 文件、归档(压缩包)、数据库连接、Socket等均需手动关闭的资源都实现了
Closer
接口。 - 编程中经常会将Close方法的调用放在
defer
语句中
type Closer interface{
Close() error //关闭数据流
}
例如:
file,err := os.Open("test.txt")
if err!=nil{
}
defer file.Close()
io.ReadCloser
-
response.Body
的类型为io.ReadCloser
io.Copy
func Copy(dst Writer, src Reader) (writtern int64, err error){
return copyBuffer(dst, src, nil)
}
-
io.Copy
方法支持在两个文件指针之间直接内容拷贝 -
io.Copy
将副本从源(读取器)复制到目标(写入器),直到源读取器达到文件末尾(EOF)或发生错误。 -
io.Copy
按默认32KB的缓存区循环操作的将源读取器复制到目标写入器 -
io.Copy
会返回复制的字节数和操作错误 -
io.Copy
抽象出for
循环模式并能正确地处理io.EOF
和字节计数 -
io.Copy
可轻松实现将数据从一个Reader
拷贝到另一个Writer
例如:当使用net/http
中下载文件时,首先会使用http.get()
创建远程请求,若直接使用ioutil.ReadAll(resp.Body)
将响应的正文直接写入文件会发现有个问题,当下载小文件时没有问题,但文件较大时,可能出现内存不足(内存溢出)的问题。
func download(url string, filepath string){
//远程请求获取内容
resp,err := http.Get(url)
if err!=nil {
panic(err)
}
reader := resp.Body
defer reader.Close()
//创建本地文件
file,err := os.Create(filepath)
if err!=nil{
panic(err)
}
defer file.Close()
writer := bufio.NewWriter(file)
//分块拷贝防止内存溢出
_,err = io.Copy(writer, reader)
if err!=nil{
panic(err)
}
writer.Flush()
}
例如:使用io.Copy
可以简化文件复制操作
//cp 复制文件
func cp(dstPath, srcPath string) (int, error){
//打开源文件读取数据
srcFile,err := os.Open(srcPath)
if err!=nil {
return 0, err
}
defer srcFile.Close()
//打开目标文件准备写入
dstFile, err := os.OpenFile(dstPath, os.O_WRONLY | os.O_CREATE, 0777)
if err!=nil {
return 0, err
}
defer dstFile.Close()
//复制操作
written := 0
buf := make([]byte, 1024)
for{
//从源文件读取
n,err := srcFile.Read(buf)
if err!=nil && err!=io.EOF{
return 0,err
}else if err==io.EOF{
break
}
//写入目标文件
dstFile.Write(buf[:n])
written += n
}
return written, nil
}
//cp 复制文件
func cp(dstPath, srcPath string) (int64, error){
//打开源文件读取数据
srcFile,err := os.Open(srcPath)
if err!=nil {
return 0, err
}
defer srcFile.Close()
//打开目标文件准备写入
dstFile, err := os.OpenFile(dstPath, os.O_WRONLY | os.O_CREATE, 0777)
if err!=nil {
return 0, err
}
defer dstFile.Close()
//复制操作
return io.Copy(dstFile, srcFile)
}
例如:实现从文件读取打印到标准输出中
func PrintFile(filename string){
file,err := os.Open(filename)
if err!=nil {
panic(err)
}
defer file.Close()
n,err := io.Copy(os.Stdout, file)
if err!=nil {
fmt.Println(n)
os.Exit(1)
}
}
io.CopyN
-
io.CopyN
会将n
字节从源复制到目标,返回复制到目标的字节数以及操作错误。 - 若源数据字节数小于指定复制的
n
字节限制,则会panic
出EOF
错误。
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
参数 | 类型 | 描述 |
---|---|---|
dst | Writer | 写入器接口实例 |
src | Reader | 读取器接口实例 |
n | int64 | 传送内容的字节数 |
例如:从字符串中读取后输入到控制台
str := "hello world\n"
reader := strings.NewReader(str)
writer := os.Stdout
n,err := io.CopyN(writer, reader, 50)
if err!=nil {
panic(err)//panic: EOF
}
fmt.Printf("\n%d bytes\n", n)
io.WriteString
-
io.WriteString()
可方便地将字符串写入一个io.Writer
中
func WriteString(w Writer, s string) (n int, err error) {
if sw, ok := w.(StringWriter); ok {
return sw.WriteString(s)
}
return w.Write([]byte(s))
}
为了操作更加安全,io.WriteString()
中进行了类型断言,若传入的Writer
对象拥有WriteString()
方法的话,则就直接调用它的WriteString()
。若没有则调用Write()
方法同时显示的进行类型转换。
参数 | 类型 | 描述 |
---|---|---|
w | Writer | 实现io.Writer 接口的写入器对象 |
s | string | 待写入的字符串 |
返回值 | 类型 | 描述 |
---|---|---|
n | int | 当前写入字符长度 |
err | error | 错误信息 |
例如:将字符串写入文件
//FileExist 判断文件是否存在
func FileExist(filepath string) bool{
if _,err := os.Stat(filepath); os.IsNotExist(err){
return false
}
return true
}
//WriteFile 写入字符串到文件
func WriteFile(filepath string, content string){
var file *os.File
var err error
//判断文件是否存在
if ok := FileExist(filepath); !ok{
//创建文件
file,err = os.Create(filepath)
if err!=nil {
panic(err)
}
}else{
//打开文件
file,err = os.OpenFile(filepath, os.O_APPEND, 0666)
if err!=nil {
panic(err)
}
}
defer file.Close()
//写入文件
_,err = io.WriteString(file, content)
if err!=nil {
panic(err)
}
}
例如:向浏览器访问的页面中输出字符串
func WriteHandler(w http.ResponseWriter, r *http.Request){
str := "hello world"
w.Write([]byte(str))
io.WriteString(w, str)
fmt.Fprintf(w, str)
return
}