go教程12

1.     读取文件

文件读取是所有编程语言中最常见的操作之一。本教程我们会学习如何使用 Go 读取文件。

本教程分为如下小节。

  • 将整个文件读取到内存
    • 使用绝对文件路径
    • 使用命令行标记来传递文件路径
    • 将文件绑定在二进制文件中
  • 分块读取文件
  • 逐行读取文件

将整个文件读取到内存

将整个文件读取到内存是最基本的文件操作之一。这需要使用 ioutil 包中的 ReadFile 函数。

让我们在 Go 程序所在的目录中,读取一个文件。我已经在 GOPATH(译注:原文是 GOROOT,应该是笔误)中创建了文件夹,在该文件夹内部,有一个文本文件 test.txt,我们会使用 Go 程序 filehandling.go 来读取它。test.txt 包含文本 “Hello World. Welcome to file handling in Go”。我的文件夹结构如下:

src
    filehandling
        filehandling.go
        test.txt

接下来我们来看看代码。

package main
 
import (
    "fmt"
    "io/ioutil"
)
 
func main() {
    data, err := ioutil.ReadFile("test.txt")
    if err != nil {
        fmt.Println("File reading error", err)
        return
    }
    fmt.Println("Contents of file:", string(data))
}

由于无法在 playground 上读取文件,因此请在你的本地环境运行这个程序。

在上述程序的第 9 行,程序会读取文件,并返回一个字节切片,而这个切片保存在 data 中。在第 14 行,我们将 data 转换为 string,显示出文件的内容。

请在 test.txt 所在的位置运行该程序。

例如,对于 linux/mac,如果 test.txt 位于 /home/naveen/go/src/filehandling,可以使用下列步骤来运行程序。

$ cd /home/naveen/go/src/filehandling/
$ go install filehandling
$ workspacepath/bin/filehandling

对于 windows,如果 test.txt 位于 C:\Users\naveen.r\go\src\filehandling,则使用下列步骤。

> cd C:\Users\naveen.r\go\src\filehandling
> go install filehandling
> workspacepath\bin\filehandling.exe

该程序会输出:

Contents of file: Hello World. Welcome to file handling in Go.

如果在其他位置运行这个程序(比如 /home/userdirectory),会打印下面的错误。

File reading error open test.txt: The system cannot find the file specified.

这是因为 Go 是编译型语言。go install 会根据源代码创建一个二进制文件。二进制文件独立于源代码,可以在任何位置上运行。由于在运行二进制文件的位置上没有找到 test.txt,因此程序会报错,提示无法找到指定的文件。

有三种方法可以解决这个问题。

  1. 使用绝对文件路径
  2. 使用命令行标记来传递文件路径
  3. 将文件绑定在二进制文件中

让我们来依次介绍。

1. 使用绝对文件路径

要解决问题,最简单的方法就是传入绝对文件路径。我已经修改了程序,把路径改成了绝对路径。

package main
 
import (
    "fmt"
    "io/ioutil"
)
 
func main() {
    data, err := ioutil.ReadFile("/home/naveen/go/src/filehandling/test.txt")
    if err != nil {
        fmt.Println("File reading error", err)
        return
    }
    fmt.Println("Contents of file:", string(data))
}

现在可以在任何位置上运行程序,打印出 test.txt 的内容。

例如,可以在我的家目录运行。

$ cd $HOME
$ go install filehandling
$ workspacepath/bin/filehandling

该程序打印出了 test.txt 的内容。

看似这是一个简单的方法,但它的缺点是:文件必须放在程序指定的路径中,否则就会出错。

2. 使用命令行标记来传递文件路径

另一种解决方案是使用命令行标记来传递文件路径。使用 flag 包,我们可以从输入的命令行获取到文件路径,接着读取文件内容。

首先我们来看看 flag 包是如何工作的。flag 包有一个名为 String 的函数。该函数接收三个参数。第一个参数是标记名,第二个是默认值,第三个是标记的简短描述。

让我们来编写程序,从命令行读取文件名。将 filehandling.go 的内容替换如下:

package main
import (
    "flag"
    "fmt"
)
 
func main() {
    fptr := flag.String("fpath", "test.txt", "file path to read from")
    flag.Parse()
    fmt.Println("value of fpath is", *fptr)
}

在上述程序中第 8 行,通过 String 函数,创建了一个字符串标记,名称是 fpath,默认值是 test.txt,描述为 file path to read from。这个函数返回存储 flag 值的字符串变量的地址。

在程序访问 flag 之前,必须先调用 flag.Parse()

在第 10 行,程序会打印出 flag 值。

使用下面命令运行程序。

wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt

我们传入 /path-of-file/test.txt,赋值给了 fpath 标记。

该程序输出:

value of fpath is /path-of-file/test.txt

这是因为 fpath 的默认值是 test.txt

现在我们知道如何从命令行读取文件路径了,让我们继续完成我们的文件读取程序。

package main
import (
    "flag"
    "fmt"
    "io/ioutil"
)
 
func main() {
    fptr := flag.String("fpath", "test.txt", "file path to read from")
    flag.Parse()
    data, err := ioutil.ReadFile(*fptr)
    if err != nil {
        fmt.Println("File reading error", err)
        return
    }
    fmt.Println("Contents of file:", string(data))
}

在上述程序里,命令行传入文件路径,程序读取了该文件的内容。使用下面命令运行该程序。

wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt

请将 /path-of-file/ 替换为 test.txt 的真实路径。该程序将打印:

Contents of file: Hello World. Welcome to file handling in Go.

3. 将文件绑定在二进制文件中

虽然从命令行获取文件路径的方法很好,但还有一种更好的解决方法。如果我们能够将文本文件捆绑在二进制文件,岂不是很棒?这就是我们下面要做的事情。

有很多可以帮助我们实现。我们会使用 packr,因为它很简单,并且我在项目中使用它时,没有出现任何问题。

第一步就是安装 packr 包。

在命令提示符中输入下面命令,安装 packr 包。

go get -u github.com/gobuffalo/packr/...

packr 会把静态文件(例如 .txt 文件)转换为 .go 文件,接下来,.go 文件会直接嵌入到二进制文件中。packer 非常智能,在开发过程中,可以从磁盘而非二进制文件中获取静态文件。在开发过程中,当仅仅静态文件变化时,可以不必重新编译。

我们通过程序来更好地理解它。用以下内容来替换 handling.go 文件。

package main
 
import (
    "fmt"
 
    "github.com/gobuffalo/packr"
)
 
func main() {
    box := packr.NewBox("../filehandling")
    data := box.String("test.txt")
    fmt.Println("Contents of file:", data)
}

在上面程序的第 10 行,我们创建了一个新盒子(New Box)。盒子表示一个文件夹,其内容会嵌入到二进制中。在这里,我指定了 filehandling 文件夹,其内容包含 test.txt。在下一行,我们读取了文件内容,并打印出来。

在开发阶段时,我们可以使用 go install 命令来运行程序。程序可以正常运行。packr 非常智能,在开发阶段可以从磁盘加载文件。

使用下面命令来运行程序。

go install filehandling
workspacepath/bin/filehandling

该命令可以在其他位置运行。packr 很聪明,可以获取传递给 NewBox 命令的目录的绝对路径。

该程序会输出:

Contents of file: Hello World. Welcome to file handling in Go.

你可以试着改变 test.txt 的内容,然后再运行 filehandling。可以看到,无需再次编译,程序打印出了 test.txt 的更新内容。完美!

现在我们来看看如何将 test.txt 打包到我们的二进制文件中。我们使用 packr 命令来实现。

运行下面的命令:

packr install -v filehandling

它会打印:

building box ../filehandling
packing file filehandling.go
packed file filehandling.go
packing file test.txt
packed file test.txt
built box ../filehandling with ["filehandling.go" "test.txt"]
filehandling

该命令将静态文件绑定到了二进制文件中。

在运行上述命令之后,使用命令 workspacepath/bin/filehandling 来运行程序。程序会打印出 test.txt 的内容。于是从二进制文件中,我们读取了 test.txt 的内容。

如果你不知道文件到底是由二进制还是磁盘来提供,我建议你删除 test.txt,并在此运行 filehandling 命令。你将看到,程序打印出了 test.txt 的内容。太棒了:D。我们已经成功将静态文件嵌入到了二进制文件中。

分块读取文件

在前面的章节,我们学习了如何把整个文件读取到内存。当文件非常大时,尤其在 RAM 存储量不足的情况下,把整个文件都读入内存是没有意义的。更好的方法是分块读取文件。这可以使用 bufio 包来完成。

让我们来编写一个程序,以 3 个字节的块为单位读取 test.txt 文件。如下所示,替换 filehandling.go 的内容。

package main
 
import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)
 
func main() {
    fptr := flag.String("fpath", "test.txt", "file path to read from")
    flag.Parse()
 
    f, err := os.Open(*fptr)
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        if err = f.Close(); err != nil {
            log.Fatal(err)
        }
    }()
    r := bufio.NewReader(f)
    b := make([]byte, 3)
    for {
        _, err := r.Read(b)
        if err != nil {
            fmt.Println("Error reading file:", err)
            break
        }
        fmt.Println(string(b))
    }
}

在上述程序的第 15 行,我们使用命令行标记传递的路径,打开文件。

在第 19 行,我们延迟了文件的关闭操作。

在上面程序的第 24 行,我们新建了一个缓冲读取器(buffered reader)。在下一行,我们创建了长度和容量为 3 的字节切片,程序会把文件的字节读取到切片中。

第 27 行的 Read 方法会读取 len(b) 个字节(达到 3 字节),并返回所读取的字节数。当到达文件最后时,它会返回一个 EOF 错误。程序的其他地方比较简单,不做解释。

如果我们使用下面命令来运行程序:

$ go install filehandling
$ wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt

会得到以下输出:

Hel
lo
Wor
ld.
 We
lco
me
to
fil
e h
and
lin
g i
n G
o.
Error reading file: EOF

本节我们讨论如何使用 Go 逐行读取文件。这可以使用 bufio 来实现。

请将 test.txt 替换为以下内容。

Hello World. Welcome to file handling in Go.
This is the second line of the file.
We have reached the end of the file.

逐行读取文件涉及到以下步骤。

  1. 打开文件;
  2. 在文件上新建一个 scanner;
  3. 扫描文件并且逐行读取。

将 filehandling.go 替换为以下内容。

package main
 
import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)
 
func main() {
    fptr := flag.String("fpath", "test.txt", "file path to read from")
    flag.Parse()
 
    f, err := os.Open(*fptr)
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        if err = f.Close(); err != nil {
        log.Fatal(err)
    }
    }()
    s := bufio.NewScanner(f)
    for s.Scan() {
        fmt.Println(s.Text())
    }
    err = s.Err()
    if err != nil {
        log.Fatal(err)
    }
}

在上述程序的第 15 行,我们用命令行标记传入的路径,打开文件。在第 24 行,我们用文件创建了一个新的 scanner。第 25 行的 Scan() 方法读取文件的下一行,如果可以读取,就可以使用 Text() 方法。

当 Scan 返回 false 时,除非已经到达文件末尾(此时 Err() 返回 nil),否则 Err() 就会返回扫描过程中出现的错误。

如果我使用下面命令来运行程序:

$ go install filehandling
$ workspacepath/bin/filehandling -fpath=/path-of-file/test.txt

程序会输出:

Hello World. Welcome to file handling in Go.
This is the second line of the file.
We have reached the end of the file.

本教程到此结束。希望你能喜欢,祝你愉快。

2.     写入文件

    在这一章我们将学习如何使用 Go 语言将数据写到文件里面。并且还要学习如何同步的写到文件里面。

这章教程包括如下几个部分:

  • 将字符串写入文件
  • 将字节写入文件
  • 将数据一行一行的写入文件
  • 追加到文件里
  • 并发写文件

请在本地运行所有本教程的程序,因为 playground 对文件的操作支持的并不好。

将字符串写入文件

最常见的写文件就是将字符串写入文件。这个写起来非常的简单。这个包含以下几个阶段。

  1. 创建文件
  2. 将字符串写入文件

我们将得到如下代码。

package main
 
import (
    "fmt"
    "os"
)
 
func main() {
    f, err := os.Create("test.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    l, err := f.WriteString("Hello World")
    if err != nil {
        fmt.Println(err)
        f.Close()
        return
    }
    fmt.Println(l, "bytes written successfully")
    err = f.Close()
    if err != nil {
        fmt.Println(err)
        return
    }
}

在第 9 行使用 create 创建一个名字为 test.txt 的文件。如果这个文件已经存在,那么 create 函数将截断这个文件。该函数返回一个文件描述符

在第 14 行,我们使用 WriteString 将字符串 Hello World 写入到文件里面。这个方法将返回相应写入的字节数,如果有错误则返回错误。

最后,在第 21 行我们将文件关闭。

上面程序将打印:

11 bytes written successfully

运行完成之后你会在程序运行的目录下发现创建了一个 test.txt 的文件。如果你使用文本编辑器打开这个文件,你可以看到文件里面有一个 Hello World 的字符串。

将字节写入文件

将字节写入文件和写入字符串非常的类似。我们将使用 Write 方法将字节写入到文件。下面的程序将一个字节的切片写入文件。

package main
 
import (
    "fmt"
    "os"
)
 
func main() {
    f, err := os.Create("/home/naveen/bytes")
    if err != nil {
        fmt.Println(err)
        return
    }
    d2 := []byte{104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}
    n2, err := f.Write(d2)
    if err != nil {
        fmt.Println(err)
        f.Close()
        return
    }
    fmt.Println(n2, "bytes written successfully")
    err = f.Close()
    if err != nil {
        fmt.Println(err)
        return
    }
}

在上面的程序中,第 15 行使用了 Write 方法将字节切片写入到 bytes 这个文件里。这个文本在目录 /home/naveen 里面。你也可以将这个目录换成其他的目录。剩余的程序自带解释。如果执行成功,这个程序将打印 11 bytes written successfully。并且创建一个 bytes 的文件。打开文件,你会发现该文件包含了文本 hello bytes

将字符串一行一行的写入文件

另外一个常用的操作就是将字符串一行一行的写入到文件。这一部分我们将写一个程序,该程序创建并写入如下内容到文件里。

Welcome to the world of Go.
Go is a compiled language.
It is easy to learn Go.

让我们看下面的代码:

package main
 
import (
    "fmt"
    "os"
)
 
func main() {
    f, err := os.Create("lines")
    if err != nil {
        fmt.Println(err)
        f.Close()
        return
    }
    d := []string{"Welcome to the world of Go1.", "Go is a compiled language.",
"It is easy to learn Go."}
 
    for _, v := range d {
        fmt.Fprintln(f, v)
        if err != nil {
            fmt.Println(err)
            return
        }
    }
    err = f.Close()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("file written successfully")
}

在上面程序的第 9 行,我们先创建一个名字叫做 lines 的文件。在第 17 行,我们用迭代并使用 for rang 循环这个数组,并使用 Fprintln Fprintln 函数 将 io.writer 做为参数,并且添加一个新的行,这个正是我们想要的。如果执行成功将打印 file written successfully,并且在当前目录将创建一个 lines 的文件。lines 这个文件的内容如下所示:

Welcome to the world of Go1.
Go is a compiled language.
It is easy to learn Go.

追加到文件

这一部分我们将追加一行到上节创建的 lines 文件中。我们将追加 File handling is easy 到 lines 这个文件。

这个文件将以追加和写的方式打开。这些标志将通过 Open 方法实现。当文件以追加的方式打开,我们添加新的行到文件里。

package main
 
import (
    "fmt"
    "os"
)
 
func main() {
    f, err := os.OpenFile("lines", os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        fmt.Println(err)
        return
    }
    newLine := "File handling is easy."
    _, err = fmt.Fprintln(f, newLine)
    if err != nil {
        fmt.Println(err)
                f.Close()
        return
    }
    err = f.Close()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("file appended successfully")
}

在上面程序的第 9 行,我们以写的方式打开文件并将一行添加到文件里。当成功打开文件之后,在程序第 15 行,我们添加一行到文件里。程序成功将打印 file appended successfully。运行程序,新的行就加到文件里面去了。

Welcome to the world of Go1.
Go is a compiled language.
It is easy to learn Go.
File handling is easy.

并发写文件

当多个 goroutines 同时(并发)写文件时,我们会遇到竞争条件(race condition)。因此,当发生同步写的时候需要一个 channel 作为一致写入的条件。

我们将写一个程序,该程序创建 100 个 goroutinues。每个 goroutinue 将并发产生一个随机数,届时将有 100 个随机数产生。这些随机数将被写入到文件里面。我们将用下面的方法解决这个问题 .

  1. 创建一个 channel 用来读和写这个随机数。
  2. 创建 100 个生产者 goroutine。每个 goroutine 将产生随机数并将随机数写入到 channel 里。
  3. 创建一个消费者 goroutine 用来从 channel 读取随机数并将它写入文件。这样的话我们就只有一个 goroutinue 向文件中写数据,从而避免竞争条件。
  4. 一旦完成则关闭文件。

我们开始写产生随机数的 produce 函数:

func produce(data chan int, wg *sync.WaitGroup) {
    n := rand.Intn(999)
    data <- n
    wg.Done()
}

上面的方法产生随机数并且将 data 写入到 channel 中,之后通过调用 waitGroup 的 Done 方法来通知任务已经完成。

让我们看看将数据写到文件的函数:

func consume(data chan int, done chan bool) {
    f, err := os.Create("concurrent")
    if err != nil {
        fmt.Println(err)
        return
    }
    for d := range data {
        _, err = fmt.Fprintln(f, d)
        if err != nil {
            fmt.Println(err)
            f.Close()
            done <- false
            return
        }
    }
    err = f.Close()
    if err != nil {
        fmt.Println(err)
        done <- false
        return
    }
    done <- true
}

这个 consume 的函数创建了一个名为 concurrent 的文件。然后从 channel 中读取随机数并且写到文件中。一旦读取完成并且将随机数写入文件后,通过往 done 这个 cahnnel 中写入 true 来通知任务已完成。

下面我们写 main 函数,并完成这个程序。下面是我提供的完整程序:

package main
 
import (
    "fmt"
    "math/rand"
    "os"
    "sync"
)
 
func produce(data chan int, wg *sync.WaitGroup) {
    n := rand.Intn(999)
    data <- n
    wg.Done()
}
 
func consume(data chan int, done chan bool) {
    f, err := os.Create("concurrent")
    if err != nil {
        fmt.Println(err)
        return
    }
    for d := range data {
        _, err = fmt.Fprintln(f, d)
        if err != nil {
            fmt.Println(err)
            f.Close()
            done <- false
            return
        }
    }
    err = f.Close()
    if err != nil {
        fmt.Println(err)
        done <- false
        return
    }
    done <- true
}
 
func main() {
    data := make(chan int)
    done := make(chan bool)
    wg := sync.WaitGroup{}
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go produce(data, &wg)
    }
    go consume(data, done)
    go func() {
        wg.Wait()
        close(data)
    }()
    d := <-done
    if d == true {
        fmt.Println("File written successfully")
    } else {
        fmt.Println("File writing failed")
    }
}

main 函数在第 41 行创建写入和读取数据的 channel,在第 42 行创建 done 这个 channel,此 channel 用于消费者 goroutinue 完成任务之后通知 main 函数。第 43 行创建 Waitgroup 的实例 wg,用于等待所有生产随机数的 goroutine 完成任务。

在第 44 行使用 for 循环创建 100 个 goroutines。在第 49 行调用 waitgroup 的 wait() 方法等待所有的 goroutines 完成随机数的生成。然后关闭 channel。当 channel 关闭时,消费者 consume goroutine 已经将所有的随机数写入文件,在第 37 行 将 true 写入 done 这个 channel 中,这个时候 main 函数解除阻塞并且打印 File written successfully

现在你可以用任何的文本编辑器打开文件 concurrent,可以看到 100 个随机数已经写入 

本教程到此结束。希望你能喜欢,祝你愉快。

 

 

其他

格式化, 用tab不是空格, 可以与go fmt 兼容

posted @ 2024-01-23 10:38  易先讯  阅读(4)  评论(0编辑  收藏  举报