Go
Linux

『ふつうのLinuxプログラミング』サンプルプログラムをGoで書いてみる② #golang

More than 1 year has passed since last update.

前回はLinuxのコマンドライン引数の扱いについて、CのコードをGoで書いてみながら学びました。
『ふつうのLinuxプログラミング』サンプルプログラムをGoで書いてみる①

今回はストリーム、ファイルを操作するプログラムをGoで書いてみます。

ストリームの定義

ふつうのLinuxプログラミング(第二版)では、ストリームは以下のように定義されています。

ファイルディスクリプタで表現され、read()またはwrite()で操作できるもの

ファイルをopen()すると、read()またはwrite()を実行できるようになるため、そこに「ストリームがある」ということができます。

catコマンドをつくる

サンプルプログラムがいきなり難しくなりました...が、処理の流れを確認しながらGoで書いてみます。

cat.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

static void do_cat(const char *path);
static void die(const char *s);

int
main(int argc, char *argv[])
{
    int i;
    if (argc <2) {
        fprintf(stderr, "%s: file name not given\n", argv[0]);
        exit(1);
    }
    for (i = 1; i < argc; i++) {
        do_cat(argv[i]);    
    }
    exit(0);
}

#define BUFFER_SIZE 2048

static void
do_cat(const char *path)
{
    int fd;
    unsigned char buf[BUFFER_SIZE];
    int n;

    fd = open(path, O_RDONLY);
    if (fd < 0) die(path);
    for(;;) {
        n = read(fd, buf, sizeof buf);
        if (n<0) die(path);
        if (n==0) break;
        if (write(STDOUT_FILENO, buf, n) < 0) die(path);
    }
    if (close(fd) < 0) die(path);
}

static void
die(const char *s)
{
    perror(s);
    exit(1);
}

出典: ふつうのLinuxプログラミング(第二版) P86

主要な処理はdo_cat関数に書かれています。

この関数でははじめに、引数で渡されるファイルを読み取り専用で開いています(open())。

ループで回しながら、ファイルの終わりまでストリームからバイト列を読み込み(read())、読み込んだバイト数のぶんだけ標準出力に書き込んでいます(write())。

処理の最後にはファイルを閉じています(close())。

また、die()という関数でエラー時の処理を行っています。
エラーが発生したらメッセージを出し、exit()しています。

Goで書いてみる

Goではファイルを開くためにosパッケージのOpen、読み込みのためにioパッケージのReader、書き込みのためにioパッケージのWriteStringを利用しました。

cat.go
package main

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

const bufferSize = 2048

func doCat(path string) error {
    fd, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("ファイルが開けませんでした%s", fd)
    }
    defer fd.Close()

    buf := make([]byte, bufferSize)
    for {
        n, err := fd.Read(buf)
        if n == 0 {
            break
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return fmt.Errorf("ファイルが読み込めませんでした%s", fd)
        }
        io.WriteString(os.Stdout, string(buf))
    }
    return nil
}

func run() error {
    argc := len(os.Args)
    argv := os.Args
    if argc < 2 {
        fmt.Fprintf(os.Stderr, "%s: ファイルを指定してください\n", argv[0])
    }
    for i := 1; i < argc; i++ {
        return doCat(argv[i])
    }
    return nil
}

func main() {
    if err := run(); err != nil {
        fmt.Fprintf(os.Stderr, "%s\n", err.Error())
        os.Exit(1)
    }
}

os.Openを使うと読み込み専用のファイルを開くことができます。

また、今回はC言語のサンプルに極力近づけるために、バッファを自分で定義してio.Readerに渡すようにしました。
バッファを厳密に定義する必要がなければ、bufioパッケージのScannerを使うのが便利だと思います。Scannerはファイルの終端までScanを実行し、終端に至ると停止します。

今回はio.EOFを使い、終端に至ったら処理を終了するようにしました。

また、読み込みを行うio.Readerは「何バイト読み込み、書き込んだか」をint型で返すため、負の数が返ることはないと考え、参考にしたC言語のサンプル内に存在した「0より小さかったらファイルを閉じる」という処理は省きました。

書き込みについても、今回はサンプルに近づけるためにio.WriteStringを使いました。
特別な目的がなければ、io.Writerを内部的に呼んでいる fmt.Printlnなどを使うのが一般的かつフォーマットもできて便利かと思います。

追記

実は本ではこのあと、システムコールではなくstdioを使ってつくるcatコマンドのサンプルがありました!!(下のcat2.cの例です)

上で書いたcat.goのコードでは、Cでシステムコールを使って直接プログラムをする例に寄せようとしたため、冗長だったり強引に書いているところがありました:sob:
そこで、改めてGoでよく見る書き方に直してみました。

cat2.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int i;
    for (i =1; i < argc; i++) {
        FILE *f;
        int c;

        f = fopen(argv[i], "r");
        if (!f) {
            perror(argv[i]);
            exit(1);
        }
        while ((c = fgetc(f)) != EOF) {
            if (putchar(c) < 0) exit(1);
        }
        fclose(f);
    }
    exit(0);
}

出典: ふつうのLinuxプログラミング(第二版) P110

cat2.go
package main

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

func doCat() error {
    argv := os.Args
    argc := len(argv)

    if argc < 2 {
        return fmt.Errorf("ファイルを指定してください")
    }

    for i := 1; i < argc; i++ {
        f, err := os.Open(argv[i])
        defer f.Close()
        if err != nil {
            return fmt.Errorf("ファイルを読み込めませんでした。%s", f)
        }
        sc := bufio.NewScanner(f)
        for sc.Scan() {
            fmt.Print(sc.Text())
        }
        if err := sc.Err(); err != nil {
            panic(err)
        }
    }
    return nil
}

func main() {
    if err := doCat(); err != nil {
        fmt.Fprintf(os.Stderr, "%s\n", err.Error())
        os.Exit(1)
    }
}

bufio.Scannerを使ってファイルから1行ずつ読み込み、fmt.Printを使ってデフォルトのフォーマットで標準出力に書き込むようにしました。

以上、もし間違い等ありましたら、コメントでご指摘いただければ幸いです :bow:

参考

前回

『ふつうのLinuxプログラミング』サンプルプログラムをGoで書いてみる①