5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-10-19

前回は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で書いてみる①

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?