前回はLinuxのコマンドライン引数の扱いについて、CのコードをGoで書いてみながら学びました。
『ふつうのLinuxプログラミング』サンプルプログラムをGoで書いてみる①
今回はストリーム、ファイルを操作するプログラムをGoで書いてみます。
ストリームの定義
ふつうのLinuxプログラミング(第二版)では、ストリームは以下のように定義されています。
ファイルディスクリプタで表現され、read()またはwrite()で操作できるもの
ファイルをopen()すると、read()またはwrite()を実行できるようになるため、そこに「ストリームがある」ということができます。
catコマンドをつくる
サンプルプログラムがいきなり難しくなりました...が、処理の流れを確認しながらGoで書いてみます。
#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を利用しました。
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でシステムコールを使って直接プログラムをする例に寄せようとしたため、冗長だったり強引に書いているところがありました
そこで、改めてGoでよく見る書き方に直してみました。
#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
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を使ってデフォルトのフォーマットで標準出力に書き込むようにしました。
以上、もし間違い等ありましたら、コメントでご指摘いただければ幸いです
参考
- GoDoc | io | Reader
- GoDoc | bufio | Scanner
- [Goならわかるシステムプログラミング ― 第2回 | 低レベルアクセスへの入り口(1):io.Writer]
(http://ascii.jp/elem/000/001/243/1243667/) - Goならわかるシステムプログラミング ― 第3回 | 低レベルアクセスへの入り口(2):io.Reader前編
- DRYな備忘録 | 【Go言語】可変長のioをReadしたい【bufio.Scanner】【io.Rader】
-
お気楽 Go 言語プログラミング入門 | ファイル入出力
github.com/tenntenn/gohandon | step3