Golang 文件操作的那些事儿

Os模块的使用与源码研究

文件:计算机中的文件是存储在外部介质(通常是磁盘)上的数据集合,文件分为文本文件和二进制文件。例如咱们常见的文件后缀名.exe,.txt,'.word'...等等

文件的基本操作可简单分为两类,也就是咱们所说的CURD(增删改查),也是基于此两类操作。可简单理解为打开文件夹CURD、关闭文件夹。结束~

golang对于文件基本上都是基于Golang的os模块,那让我们一起了解一下,那么Golang是如何对文件进行操作呢。Let's Go~

打开文件

Golang中打开文件使用os.Open模块,官方os.open部分源码如下:

// os.Open
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

Open打开命名文件以供读取。如果成功,则可以使用返回文件上的方法进行读取;关联的文件。描述符的模式为O_RDONLY。 如果有错误,它将是* PathError类型。

它接收一个string 类型的变量name,返回两个值,File的指针和错误error。那么我们使用它打开文件的的时候就需要这样做

fileObj, err := os.Open(name string)
// 其中os.Open中的name为路径Path

基础使用的介绍暂且为止,其实我们更应该关心的应该是OpenFile(name, O_RDONLY, 0),这个函数到底干了啥,我们追踪一下这个函数(在GoLang编辑器中, mac可以直接使用command + 鼠标左键直接进入,Win可以使用ctrl + 鼠标左键),如下:

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    testlog.Open(name)
    f, err := openFileNolog(name, flag, perm)
    if err != nil {
        return nil, err
    }
    f.appendMode = flag&O_APPEND != 0

    return f, nil
}
// OpenFile是广义的open调用;大多数用户将使用Open 或Create代替。它打开带有指定标志的命名文件(O_RDONLY等)。如果该文件不存在,并且传递了O_CREATE标志,则会使用模式perm(在umask之前)创建该文件。如果成功,返回文件上的方法可以用于I / O。 如果有错误,它将是* PathError类型。

这个文件全部内容还是有点分量的,有信息的伙伴,可以详细的阅读一下全部内容。暂且为止

那让我们实践一下,使用Golang打开文件,如下

package main

import (
    "fmt"
    "os"
)

func main() {
    // 打开此文件,./main.go为相对路径。在这里是此文件
    fileObj, err := os.Open("./main.go")
    // 异常处理
    if err != nil {
        fmt.Printf("Open File Error Message:%#v\n", err)
        return
    }
    // 尝试打印(此处输出的为地址值)
    fmt.Println(&fileObj)
    // defer 关闭文件
    defer fileObj.Close()
}

image

以防忘记关闭文件,造成bug,我们在这里使用defer + 关闭。

注意:在编辑器中并不建议直接使用鼠标右键运行,这样可能会导致路径错误。大部分的编辑器都并不是只运行此文件!!!

Open File Error Message:&os.PathError{Op:"open", Path:"./main.go", Err:0x2}

如果你遇见了类似的错误,你可以直接在终端中,切换到当前路径。使用go run main.go,直接运行。这样就可以直接得到正确的结果啦

读取文件

打开文件之后,那么我们可以就可以对他们进行操作了,我们在这里主要演示一下读取文件的操作。还是老样子,先看一下主要的相关源码,如下:

// FileObj.Read()
func (f *File) Read(b []byte) (n int, err error) {
    if err := f.checkValid("read"); err != nil {
        return 0, err
    }
    n, e := f.read(b)
    return n, f.wrapErr("read", e)
}

// f.read(b)
func (f *File) read(b []byte) (n int, err error) {
    n, err = f.pfd.Read(b)
    runtime.KeepAlive(f)
    return n, err
}

FileObj.Read()

示例化接受文件的地址值(也就是咱们前面打开获取到的结果),接受切片的字节,返回读取的内容,以及错误

在此函数中首先检查是否为有效的读取,然后在进行f.read(b)的操作,接受其返回结果。

f.read(b)

在这里,主要检测是否在读取,如果是那么返回本次的读取内容

从以上我们不难看出,其实读取文件是读取文件内部的字节

那么更具FileObj.Read(),我们可以了解它基本的使用方法,如下

func (f *File) Read(b []byte) (n int, err error)

读取部分的示例代码如下:

在这里我们需要考虑:是否能够正常读取?是否读完了?具体请看异常处理部分

// 读取文件
    // 定义每次读取的大小
    //var tmp = make([]byte, 128)
    var tmp  [128]byte

    // n:从开始到结尾的内容
    n, err := fileObj.Read(tmp[:])
    // 异常处理
    if err != nil {
        fmt.Printf("Read of File Error, ErrorMessage:%#v\n", err)
        return
    }
    if err == io.EOF {
        fmt.Println("文件读完了")
        return
    }
    fmt.Printf("读取了%d个字节\n", n)
    fmt.Printf("读取到的内容:\n%s",tmp[:])

输出结果如下:

image

以上很明显是并没有读完的仅读取了部分,原始的全部代码如下

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开此文件,./main.go为相对路径。在这里是此文件
    fileObj, err := os.Open("./main.go")
    // 异常处理
    if err != nil {
        fmt.Printf("Open of File Error, ErrorMessage:%#v\n", err)
        return
    }
    // 尝试打印(此处输出的为地址值)
    fmt.Println(&fileObj)
    // defer 关闭文件
    defer fileObj.Close()

    // 读取文件
    // 定义每次读取的大小
    //var tmp = make([]byte, 128)
    var tmp  [128]byte

    // n:从开始到结尾的内容
    n, err := fileObj.Read(tmp[:])
    // 异常处理
    if err != nil {
        fmt.Printf("Read of File Error, ErrorMessage:%#v\n", err)
        return
    }
    if err == io.EOF {
        fmt.Println("文件读完了")
        return
    }
    fmt.Printf("读取了%d个字节\n", n)
    fmt.Printf("读取到的内容:\n%s",tmp[:])
}

完整读取

for无线循环读取

由于以上我们并没有读取完整个文件,那么我需要读取全部的该怎么办呢?一个方法是不断的读取下去,然后和在一起就是完整的内容了,示例代码如下

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开此文件,./main.go为相对路径。在这里是此文件
    fileObj, err := os.Open("./main.go")
    // 异常处理
    if err != nil {
        fmt.Printf("Open of File Error, ErrorMessage:%#v\n", err)
        return
    }
    // 尝试打印(此处输出的为地址值)
    fmt.Println(&fileObj)
    // defer 关闭文件
    defer fileObj.Close()
    // 循环读取文件
    var content []byte
    var tmp = make([]byte, 128)
    for {
        n, err := fileObj.Read(tmp)
        if err == io.EOF {
            fmt.Println("文件读完了")
            break
        }
        if err != nil {
            fmt.Printf("Read of File Error, ErrorMessage:%#v\n", err)
            return
        }
        content = append(content, tmp[:n]...)
    }
    fmt.Println(string(content))
}

主要的思路为:无限循环去读取,读完了之后break掉。然后把读取的内容合并起来

这种读取虽然可行,不过是否有点太麻烦了,那么有什么更简便的方式呢?答案当然是有的,bufio读取

bufio读取

bufio是在file的基础上封装了一层API,支持更多的功能。

主要的部分源码如下所示

// bufio.NewReader
// NewReader returns a new Reader whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
    return NewReaderSize(rd, defaultBufSize)
}

// NewReaderSize
// NewReaderSize returns a new Reader whose buffer has at least the specified
// size. If the argument io.Reader is already a Reader with large enough
// size, it returns the underlying Reader.
func NewReaderSize(rd io.Reader, size int) *Reader {
    // Is it already a Reader?
    b, ok := rd.(*Reader)
    if ok && len(b.buf) >= size {
        return b
    }
    if size < minReadBufferSize {
        size = minReadBufferSize
    }
    r := new(Reader)
    r.reset(make([]byte, size), rd)
    return r
}

它简便的原因是因为已经帮我们定义了文件的指针,以及它还定义了缓冲区,这样我们使用它来读取更加的快与便捷。

bufio.NewReader语法格式

func NewReader(rd io.Reader) *Reader 
// 其中rd为我们打开文件的对象

使用如下

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开此文件,./main.go为相对路径。在这里是此文件
    fileObj, err := os.Open("./main.go")
    // 异常处理
    if err != nil {
        fmt.Printf("Open of File Error, ErrorMessage:%#v\n", err)
        return
    }
    // 尝试打印(此处输出的为地址值)
    fmt.Println(&fileObj)
    // defer 关闭文件
    defer fileObj.Close()
    // bufio读取
    reader := bufio.NewReader(fileObj)
    for {
        line, err := reader.ReadString('\n') //注意是字符
        if err == io.EOF {
            if len(line) != 0 {
                fmt.Println(line)
            }
            fmt.Println("文件读完了")
            break
        }
        if err != nil {
            fmt.Println("read file failed, err:", err)
            return
        }
        fmt.Print(line)
    }
}

输入结果如上,略。。。

搞了这么多,就没有一键读取的么?当然也是有的,让我们来了体验一下ioutil读取整个文件的愉悦。

package main

import (
    "fmt"
    "io/ioutil"
)

// ioutil.ReadFile读取整个文件
func main() {
    content, err := ioutil.ReadFile("./main.go")
    if err != nil {
        fmt.Println("read file failed, err:", err)
        return
    }
    fmt.Println(string(content))
}

其内部的实现原理,先预测整个文件的大小。然后一次性全部读取。当然需要做好异常的准备哦

// ReadFile reads the file named by filename and returns the contents.
// A successful call returns err == nil, not err == EOF. Because ReadFile
// reads the whole file, it does not treat an EOF from Read as an error
// to be reported.
func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    // It's a good but not certain bet that FileInfo will tell us exactly how much to
    // read, so let's try it but be prepared for the answer to be wrong.
    var n int64 = bytes.MinRead

    if fi, err := f.Stat(); err == nil {
        // As initial capacity for readAll, use Size + a little extra in case Size
        // is zero, and to avoid another allocation after Read has filled the
        // buffer. The readAll call will read into its allocated internal buffer
        // cheaply. If the size was wrong, we'll either waste some space off the end
        // or reallocate as needed, but in the overwhelmingly common case we'll get
        // it just right.
        if size := fi.Size() + bytes.MinRead; size > n {
            n = size
        }
    }
    return readAll(f, n)
}

文件写入操作

os.OpenFile()函数能够以指定模式打开文件,从而实现文件写入相关功能。

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    ...
}

其中:

name:要打开的文件名 flag:打开文件的模式。 模式有以下几种:

模式 含义
os.O_WRONLY 只写
os.O_CREATE 创建文件
os.O_RDONLY 只读
os.O_RDWR 读写
os.O_TRUNC 清空
os.O_APPEND 追加

perm:文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01。

Write和WriteString

func main() {
    file, err := os.OpenFile(test.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    str := "hello"
    file.Write([]byte(str))       //写入字节切片数据
    file.WriteString("hello") //直接写入字符串数据
}

bufio.NewWriter

func main() {
    file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    writer := bufio.NewWriter(file)
    for i := 0; i < 10; i++ {
        writer.WriteString("hello") //将数据先写入缓存
    }
    writer.Flush() //将缓存中的内容写入文件
}

ioutil.WriteFile

func main() {
    str := "hello"
    err := ioutil.WriteFile("./asd.txt", []byte(str), 0666)
    if err != nil {
        fmt.Println("write file failed, err:", err)
        return
    }
}

so cool~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容