M.Hiroi's Home Page

Go Language Programming

お気楽 Go 言語プログラミング入門

[ PrevPage | Golang | NextPage ]

ファイル入出力

今回はデータの入出力について簡単に説明します。Go 言語は「ファイルディスクプリタ」というデータを介してファイルにアクセスします。ファイルディスクプリタはファイルと一対一に対応していて、ファイルからデータを入力する場合は、ファイルディスクプリタを経由してデータが渡されます。逆に、ファイルへデータを出力するときも、ファイルディスクプリタを経由して行われます。

ファイルディスクプリタはパッケージ os に定義されている構造体 File に格納されてユーザーに渡されます。ただし、Go 言語の File には低レベルな処理 (メソッド) しか用意されていません。Go 言語の場合、パッケージ bufio に高レベルな処理を行うための構造体 Reader や Writer が用意されているので、File からそれらの型を生成して入出力処理を行います。

●標準入出力

通常のファイルは、ファイルディスクプリタ (File 構造体) を生成しないとアクセスすることはできません。ただし、標準入出力は Go 言語の起動時に File 構造体が自動的に生成されるので、簡単に利用することができます。一般に、キーボードからの入力を「標準入力」、画面への出力を「標準出力」といいます。標準入出力に対応する File 構造体は、パッケージ os の変数に格納されています。下表に変数名を示します。

表 : 標準入出力
変数名ファイル
Stdin 標準入力
Stdout 標準出力
Stderr 標準エラー出力

ファイルのアクセスは標準入出力を使うと簡単です。File 構造体のメソッドで、入出力の基本となるのが Read と Write です。

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

メソッド Read は f からデータをバッファ b に len(b) バイト読み込みます。正常にデータを読み込んだ場合、返り値は読み込んだバイト数と nil で、エラーが発生した場合は 0 とエラーを表す値 err を返します。たとえば、ファイルの終了を検出した場合、Read は 0 とエラー io.EOF を返します。

型 error はインターフェースで、そのメソッド Error はエラーメッセージを文字列として返します。

リスト : error の定義

type error interface {
    Error() string
}

Print や Println は error を受け取るとメソッド Error を呼び出して、返り値の文字列を表示します。

Write はバッファ b に格納されている len(b) バイト分のデータを f へ書き込みます。返り値は書き込んだバイト数で、エラーが発生した場合は 0 とエラー err を返します。

それでは簡単な例題として、入力されたデータをそのまま画面へ出力するコマンド echo を作ってみましょう。プログラムは次のようになります。

リスト : echo (echo.go)

package main

import "os"

func main() {
    buff := make([]byte, 256)
    for {
        c, _ := os.Stdin.Read(buff)
        if c == 0 { break }
        os.Stdout.Write(buff[:c])
    }
}

make で大きさ 256 バイトのバッファを用意して変数 buff にセットします。次の for ループの中で、標準入力から Read でデータを読み込み、データ数を変数 c にセットします。c が 0 ならば break で for ループを脱出します。そうでなければ、buff[:c] で c バイトのスライスを生成して、それを Write で標準出力へ書き込みます。

それでは実行してみましょう。

$ go run echo.go
hello, world    <- 入力
hello, world
foo bar baz     <- 入力
foo bar baz
sayonara        <- 入力
sayonara
                <- CTRL-C を入力

$ go run echo.go < echo.go
package main

import "os"

func main() {
    buff := make([]byte, 256)
    for {
        c, _ := os.Stdin.Read(buff)
        if c == 0 { break }
        os.Stdout.Write(buff[:c])
    }
}

ファイルをリダイレクトすることで、ファイルの内容を表示することができます。

●バイト単位の入出力

バイト単位や行単位で入出力を行いたい場合はパッケージ bufio を使うと便利です。bufio はバッファ付きの入出力処理を行います。これはC言語の FILE 構造体を使った高水準入出力処理に近い動作になります。

バイト単位で入出力を行うには、メソッド ReadByte と WriteByte を使います。

func (b *Reader) ReadByte() (c byte, err error)
func (b *Writeer) WriteByte(c byte) error

Reader と Writer は bufio に定義されている構造体です。Reader と Writer は次の関数で生成します。

func NewReader(r io.Reader) *Reader
func NewWriter(w io.Writer) *Writer

io.Reader と io.Writer はパッケージ io に定義されているインターフェースです。

リスト : io.Reader と io.Writer の定義

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

メソッド Read と Write は、File 構造体のメソッド Read と Write と同じ型です。したがって、File 型は io.Reader 型や io.Writer 型として扱うことができます。つまり、NewReader に File を渡せばそれに対応する Reader を、newWriter に File を渡せば Writer を生成することができます。

それでは、ReadByte と WriteByte を使って echo を書き直してみましょう。プログラムは次のようになります。

リスト : echo (echo1.go)

package main

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

func main() {
    r := bufio.NewReader(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    for {
        c, err := r.ReadByte()
        if err == io.EOF { break }
        w.WriteByte(c)
        if c == '\n' { w.Flush() }
    }
    w.Flush()
}

NewReader に Stdin を、NewWriter に Stdout を渡して、Reader と Writer を生成します。変数 r が Reader で、変数 w が Writer です。次の for ループの中で、ReadByte を呼び出して 1 バイト読み込み、変数 c にセットします。err が io.EOF であればファイルの終了を検出したので、break で for ループを脱出します。そうでなければ、WriteByte で c を出力します。

Writer はバッファが満杯になるまでデータを溜め込みます。実際にファイルに書き込むのはバッファが満杯になってからです。途中でバッファのデータをファイルに出力したい場合はメソッド Flush を使います。c が '\n' と等しい場合は Flush でバッファのデータを出力します。これで 1 行ごとにデータを出力することができます。最後に、Flush でバッファ内のデータを出力します。これで echo.go と同じ動作になります。

●行単位の入出力

ファイルを行単位で処理したい場合は、次のメソッドを使うと便利です。

func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
func (b *Reader) ReadString(delim byte) (line string, err error)
func (b *Writer) WriteString(s string) (n int, err error)

ReadBytes と ReadString は引数 delim で指定した区切り文字までデータを読み込みます。ReadBytes は読み込んだデータをスライスに格納して返します。ReadString は文字列にして返します。どちらのメソッドも区切り文字は捨てずにスライス (または文字列) に格納することに注意してください。

WriteString は引数の文字列 s を出力します。なお、Reader と Writer には File と同じ型のメソッド Read と Write が定義されているので、スライスに格納されているデータを出力することは Write を使って簡単に行うことができます。

echo.go を ReadString, WriteString を使って書き直すと次のようになります。

リスト : echo (echo2.go)

package main

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

func main() {
    r := bufio.NewReader(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    for {
        s, err := r.ReadString('\n')
        if err == io.EOF { break }
        w.WriteString(s)
        w.Flush()
    }
}

このように、ReadString で区切り文字に '\n' を指定すれば、行単位でファイルの読み書きを行うことができます。

●bufio.Scanner

このほかに、bufio.Scanner を使ってファイルからデータを読み込むことができます。

func NewScanner(r io.Reader) *Scanner

Scanner は指定された分割関数を使って入力データをトークンに切り分ける働きをします。関数 NewScanner は新しい Scanner を生成して返します。トークンの切り分けはメソッド Scan で行います。

func (s *Scanner) Scan() bool
func (s *Scanner) Bytes() []byte
func (s *Scanner) Text() string

Scan は Scanner に設定されている分割関数を使って次のトークンまでデータを読み込みます。あらかじめ bufio に用意されている分割関数を以下に示します。

分割関数のデフォルトは ScanLines で、入力データを行単位で分割します。この場合、改行文字は捨てられることに注意してください。分割関数の設定はメソッド Split で行います。

func (s *Scanner) Split(split SplitFunc)

読み込んだデータ (トークン) は Bytes または Text で取得することができます。Scan はファイルの終了 (EOF) を検出する、またはファイルの読み込みでエラーが発生した場合、処理を終了して false を返します。EOF 以外のエラーはメソッド Err でチェックすることができます。

func (s *Scanner) Err() error

エラーが EOF の場合、Err は nil を返すことに注意してください。

Scanner を使って echo2.go を書き直すと次のようになります。

リスト : echo (echo3.go)

package main

import (
    "os"
    "bufio"
)

func main() {
    s := bufio.NewScanner(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    for s.Scan() {
        w.WriteString(s.Text())
        w.WriteString("\n")
        w.Flush()
    }
}

s.Text() で文字列を取得しますが、改行文字が取り除かれているので、WriteString で "\n" を付加しています。この場合、あとで説明する書式付き出力関数 (Fprintf) を使うといいでしょう。

●Windows の改行文字

一般に、テキストファイルの「行」は改行文字で区切られます。改行文字は OS によって異なります。'\n' (0x0a) までを 1 行とする仕様は UNIX 系 の OS で、Windows は '\r\n' (0x0d, 0x0a) の 2 バイトで改行を表します。

この違いを吸収するため、たとえばC言語では、ファイルのアクセスモードに「テキストモード」と「バイナリモード」が用意されています。次に示すように、テキストモードでは改行コードの変換が行われます。

読み込み時: '\r\n' ─→  '\n'
書き込み時: '\n'   ─→  '\r\n'

バイナリモードの場合、改行コードの変換は行われません。

ところが Go 言語の場合、Windows でも改行文字の扱いは UNIX 系の OS と同じようです。たとえば、ReadString('\n') で 1 行読み込むと文字列の末尾には '\r\n' が付きます。逆に、Println で出力される改行文字は '\n' だけです。プログラムによっては、改行文字の違いで問題が発生する場合があるかもしれません。ご注意くださいませ。

●ファイルのアクセス方法

標準入出力を使わずにファイルにアクセスする場合、次の 3 つの操作が基本になります。

  1. アクセスするファイルをオープンする
  2. 入出力関数(メソッド)を使ってファイルを読み書きする。
  3. ファイルをクローズする。

「ファイルをオープンする」とは、アクセスするファイルを指定して、それと一対一に対応するファイルディスクプリタを生成することです。入出力関数は、そのファイルディスクプリタを経由してファイルにアクセスします。

ファイルをオープンするには関数 os.Open と os.Create を使います。

func Open(name string) (file *File, err error)
func Create(name string) (file *File, err error)

Open は引数 name で指定されたファイルをリードモードでオープンします。Create はライトモードでオープンします。正常にファイルをオープンできた場合、返り値は File 構造体へのポインタ *File と nil です。エラーが発生した場合は err に nil 以外の値がセットされます。リードモードの場合、ファイルが存在しないとエラーになります。ライトモードの場合、ファイルが存在すれば、そのファイルのサイズを 0 に切り詰めてからオープンします。

オープンしたファイルは必ずクローズしてください。それを行うメソッドが os.Close です。

func (f *File) Close() error

正常にファイルをクローズした場合、返り値は nil になります。

簡単な使用例を示します。

リスト : ファイルのオープンとクローズ

package main

import (
    "os"
    "fmt"
)

func main() {
    input, err := os.Open("testin.txt")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    output, _ := os.Create("testout.txt")
    buff := make([]byte, 256)
    for {
        c, _ := input.Read(buff)
        if c == 0 { break }
        output.Write(buff[:c])
    }
    input.Close()
    output.Close()
}

最初に testin.txt をリードモードでオープンします。err が nil でない場合、fmt.Fprintln で os.Stderr にエラーメッセージを出力して os.Exit(1) でプログラムを終了します。

Fprint, Fprintln は第 1 引数の io.Writer にデータを書き込む関数です。

func Fprint(w io.Writer, a... interface{}) (n int, err error)
func Fprintln(w io.Writer, a... interface{}) (n int, err error)

Exit はプログラムの実行を終了する関数です。引数はプログラムの終了コードを表していて、正常に終了した場合は 0 を、異常終了した場合は 0 以外の値、一般的には 1 を返すことが多いです。

あとは for ループで testin.txt からデータを読み込み、それを testout.txt へ書き込みます。最後に Close でファイルをクローズします。これで testin.txt を testout.txt にコピーすることができます。

●コマンドライン引数の取得

Go 言語の場合、パッケージ os の変数 Args にコマンドラインで与えられた引数が格納されています。次のリストを見てください。

リスト : 変数 Args の表示 (sample111.go)

package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Println(os.Args)
}

sample111.go は変数 Args の内容を表示するだけです。3 つの引数を与えて起動すると、次のように表示されます。

$ go build sample111.go

$ ./sample111 foo bar baz
[./sample111 foo bar baz]

スライスの先頭要素は実行したファイル名 (sample111) になることに注意してください。

簡単な例として、複数のファイルを表示するコマンド cat を作ってみましょう。

リスト : ファイルの表示 (cat.go)

package main

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

func cat(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    s := bufio.NewScanner(file)
    for s.Scan() {
        fmt.Println(s.Text())
    }
    file.Close()
}

func main() {
    for _, name := range os.Args[1:] {
        cat(name)
    }
}

Args[1:] でファイル名を格納したスライスを取り出し、そこから for ループでファイル名を取得して関数 cat を呼び出します。cat は引数の filename をリードモードでオープンします。ファイルが見つからない場合はエラー終了します。あとは、NewScanner で Scanner を生成して for ループで 1 行ずつ読み込んで、それを Println で標準出力へ書き込むだけです。最後に file をクローズします。

●ReadFile と WriteFile

パッケージ io/ioutil の関数 ReadFile を使うと、ファイルをすべて読み込んでスライス [ ]byte に格納して返します。ファイルのオープンとクロースは ReadFile が行ってくれます。関数 WriteFile はスライス [ ]byte の全データをファイルに書き込みます。

func ReadFile(name string) (b []byte, err error)
func WriteFile(name string, b []byte, perm os.FileMode) error

WriteFile の引数 perm はパーミッションを整数値で指定します。os.FileMode は uint32 の別名です。UNIX 系の OS では、8 進数でパーミッションを指定します。たとえば 0644 とすると、そのファイルに対して所有者は読み書きすることができて、グループに所属する人と他の全員には読むことだけが許可されます。

関数 ReadFile を使うと、cat はとても簡単にプログラムすることができます。次のリストを見てください。

リスト : ファイルの表示 (2)

package main

import (
    "os"
    "fmt"
    "io/ioutil"
)

func cat(filename string) {
    buff, err := ioutil.ReadFile(filename)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    os.Stdout.Write(buff)
}

func main() {
    for _, name := range os.Args[1:] {
        cat(name)
    }
}

ReadFile でファイル filename のデータをすべて読み込み、それを Write で標準出力へ書き込むだけです。最近のパソコンはハイスペックなので、よほど大きなファイルでなければ、ReadFile でデータをすべて読み込んで処理することは十分に可能だと思います。

●書式付き出力 Printf

Print, Fprint はデータをそのまま出力しますが、整形して出力したい場合は書式付き出力関数 Printf, Fprintf を使います。これはC言語の標準ライブラリ関数 printf と同様の機能です。

func Printf(format string, a... interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a... interface{}) (n int, err error)

引数の文字列 format を書式文字列といい、出力に関する様々な指定を行います。書式文字列はそのまま文字列として扱われますが、文字列の途中にパーセント % が表れると、その後ろの文字を変換指示子として解釈し、引数に与えられたデータをその指示に従って表示します。

簡単な例を示しましょう。

リスト : 整数の出力 (sample112.go)

package main

import "fmt"

func main() {
    fmt.Printf("%d, %x, %o\n", 100, 100, 100)
    fmt.Printf("[%d]\n", 10)
    fmt.Printf("[%4d]\n", 10)
    fmt.Printf("[%4d]\n", 100000)
    fmt.Printf("[%4d]\n", 123456)
    fmt.Printf("[%-8d]\n", 123456)
    fmt.Printf("[%08d]\n", 123456)
}
$ go run sample112.go
100, 64, 144
[10]
[  10]
[123456]
[123456  ]
[00123456]

% の次の文字 d, x, o が変換指示子です。これらの指示子は整数値を表示する働きをします。例が示すように、d は 10 進数、x は 16 進数、o は 8 進数で表示します。変換指示子の個数と与えるデータの数が合わないとエラーになるので注意してください。% を出力したい場合は %% と続けて書きます。

% と変換指示子の間にオプションでいろいろな設定を行うことができます。整数値を表示する変換指示子の場合、データを表示するフィールドの桁数を指定することができます。%d はフィールド幅を指定しない場合で、%4d がフィールド幅を 4 に指定した場合です。10 ではフィールド幅に満たないので、右詰めに出力されています。もし、フィールド幅に収まらない場合は、指定を無視して数値を出力します。フィールド幅を 0 で埋めたい場合は、フィールド桁数の前に 0 を指定します。左詰めにしたい場合は、フィールド桁数の前に - を指定します。

v 変換子は Go 言語の任意のデータをデフォルトの形式で表示します。構造体を表示するとき、v の前に + を付けるとフィールド名も出力されます。# を付けると、値を Go 言語の構文で表示します。T 変換子は値の型を表示します。

簡単な例を示しましょう。

リスト : 任意のデータと型の表示 (sample113.go)

package main

import "fmt"

type Foo struct {
    bar, baz int
}

func main() {
    a := 123456789
    b := Foo{10, 20}
    fmt.Printf("%v\n", a)
    fmt.Printf("%T\n", a)
    fmt.Printf("%v\n", b)
    fmt.Printf("%+v\n", b)
    fmt.Printf("%#v\n", b)
    fmt.Printf("%T\n", b)
}
$ go run sample113.go
123456789
int
{10 20}
{bar:10 baz:20}
main.Foo{bar:10, baz:20}
main.Foo

s 変換子は文字列または byte 型のスライス ([ ]byte) を表示します。q 変換子は Go 言語の構文で文字列または byte 型のスライスを表示します。s, q 変換子の場合でも、フィールド幅を指定することができます。簡単な例を示しましょう。

リスト : 文字列の表示 (sample114.go)

package main

import "fmt"

func main() {
    a := "hello, world"
    fmt.Printf("[%s]\n", a)
    fmt.Printf("[%20s]\n", a)
    fmt.Printf("[%-20s]\n", a)
    fmt.Printf("[%q]\n", a)
    fmt.Printf("[%20q]\n", a)
    fmt.Printf("[%-20q]\n", a)
}
$ go run sample114.go
[hello, world]
[        hello, world]
[hello, world        ]
["hello, world"]
[      "hello, world"]
["hello, world"      ]

このほかにも、変換した結果を文字列にして返す関数や浮動小数点数を表示する変換子など、fmt パッケージにはいろいろな機能があります。詳細は Go 言語 fmt パッケージのマニュアルをお読みください。

●Scan と Fscan

ファイルからデータを読み込む場合、データを文字列やスライス ([ ]byte) ではなく数値や他の型に変換できると便利です。Go 言語の場合、fmt パッケージの関数 Scan と Fscan を使うと、入力データの変換を簡単に行うことができます。

func Scan(a... interface{}) (n int, err error)
fnnc Fscan(r io.Reader, a... interface{}) (n int, err error)

Scan は標準入力から、Fscan は io.Reader からデータを読み込み、可変長引数で指定された変数の型に従ってデータ変換を行い、その値を変数に格納します。可変長引数に渡す引数はポインタ変数でなければいけません。Scan と Fscanf はデータが空白文字 (改行やタブも含む) で区切られていることを前提に動作します。返り値は変換に成功したデータ数で、正常に変換できない場合は err に nil 以外の値がセットされます。

簡単な使用例を示します。

リスト : Scan の使用例 (sample115.go)

package main

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

func main() {
    sumi := 0
    sumf := 0.0
    suma := make([]string, 0)
    for {
        var n int
        var m float64
        var s string
        i, err := fmt.Scan(&n, &m, &s)
        if i == 3 {
            sumi += n
            sumf += m
            suma = append(suma, s)
        } else if i == 0 && err == io.EOF {
            break
        } else {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
    }
    fmt.Println(sumi, sumf, suma)
}

整数、実数、文字列が順番に格納されているファイルを読み込み、整数の合計値、実数の合計値を求め、文字列はスライスに格納して表示します。sumi, sumf, は整数と実数の合計値、suma は文字列を格納するスライスです。

for ループの中で、読み込んだデータを格納する変数 n, m, s を宣言します。そして、Scan にそれらの変数へのポインタを渡します。返り値 i が 3 ならば、正常にデータを読み込むことができました。n, m は sumi, sumf に加算し、s は append で suma に追加します。i が 0 で err が io.EOF の場合、ファイルを最後まで読み込みました。break で for ループを脱出します。それ以外の場合はエラーが発生したので、エラーメッセージを表示してプログラムを終了します。

それでは簡単な実行例を示します。

10 1.234 foo
20 5.678 bar
30 9.876 baz

図 : test.dat
$ go run sample115.go < test.dat
60 16.788 [foo bar baz]

●書式付き入力 Scanf

Scanf と Fscanf は Printf と Fprintf の入力バージョンです。

func Scanf(fromat string, a... interface{}) (n int, err error)
func Fscanf(r io.Reader, fromat string, a... interface{}) (n int, err error)

書式文字列の変換指示子は、基本的には書式付き出力と同じです。変換指示子以外の文字は入力データと照合されます。このとき、改行文字 ('\n') 以外の空白文字は区切り文字として扱われ、書式文字列と入力データともに、連続した空白文字は 1 文字の空白文字になります。それ以外の文字で照合に失敗したら変換は失敗します。ただし、改行文字は空白文字として扱われないので、1 行ずつ読み込む場合は書式文字列の最後に改行文字を指定する必要があります。

たとえば、カンマで区切られたデータを読み込むプログラムは次のようになります。

リスト : Scanf の使用例 (sample116.go)

package main

import (
    "fmt"
    "os"
)

func main() {
    sumi := 0
    sumf := 0.0
    suma := make([]string, 0)
    for {
        var n int
        var m float64
        var s string
        i, err := fmt.Scanf("%d,%f,%q\n", &n, &m, &s)
        if i == 3 {
            sumi += n
            sumf += m
            suma = append(suma, s)
        } else if i == 0 {
            break
        } else {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
    }
    fmt.Println(sumi, sumf, suma)
}

書式文字列の %f は浮動小数点数を表す変換指示子です。

それでは簡単な実行例を示します。

10,1.234,"foo"
20,5.678,"bar"
30,9.876,"baz"

図 : test1.dat
$ go run sample116.go < test1.dat
60 16.788 [foo bar baz]

このほかにも、文字列からデータを読み込む関数など、fmt パッケージにはいろいろな機能があります。詳細は Go 言語 fmt パッケージのマニュアルをお読みください。

●ファイルを行単位で連結する

最後に簡単な例題として、ファイルを行単位で連結するコマンドを作りましょう。名前は paste.go としました。動作例を図に示します。

$ cat file1.txt
abcd
efgh
ijkl

$ cat file2.txt
ABCD
EFGH
IJKL

$ ./paste file1.txt file2.txt
abcdABCD
efghEFGH
ijklIJKL

図 : 行単位でファイルを連結する

paste.go は 2 つのファイル file1.txt と file2.txt の各行を連結して標準出力へ出力します。この場合、2 つのファイルを同時にオープンしなければいけませんが、近代的なプログラミング言語であれば特別なことをしなくても複数のファイルを扱うことができます。

2 つのファイルをリードモードでオープンし、それから 2 つの Reader を生成します。生成された Reader を別々の変数 file1, file2 にセットします。変数 file1 に ReadString を適用すれば、ファイル 1 から 1 行分データをリードすることができます。同様に、変数 file2 に ReadString を適用すれば、ファイル 2 からデータをリードできるのです。あとは、文字列を連結して標準出力へ出力すればいいわけです。

ただし、一つだけ注意点があります。それは、2 つのファイルの行数は同じとは限らないということです。つまり、どちらかのファイルが先に終了する場合があるのです。この場合は、残ったファイルをそのまま出力します。処理内容を下図に示します。

ファイル 1 が終了した場合は、ファイル 2 をそのまま出力します。ファイル 2 が終了した場合は、ファイル 1 をそのまま出力しますが、その前に入力したファイル 1 のデータが残っているので、読み込んだデータを出力することをお忘れなく。

それでは、プログラムを作りましょう。次のリストを見てください。

リスト : 行の結合

package main

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

func outputFile(s *bufio.Scanner) {
    for s.Scan() {
        fmt.Println(s.Text())
    }
}

func paste(s1 *bufio.Scanner, s2 *bufio.Scanner) {
    for {
        if !s1.Scan() {
            outputFile(s2)
            break
        }
        if !s2.Scan() {
            fmt.Println(s1.Text())
            outputFile(s1)
            break
        }
        fmt.Printf("%s%s\n", s1.Text(), s2.Text())
    }
}

func main() {
    if len(os.Args) < 3 {
        fmt.Fprint(os.Stderr, "not enough args\n")
        os.Exit(1)
    }
    file1, err1 := os.Open(os.Args[1])
    if err1 != nil {
        fmt.Fprint(os.Stderr, err1)
        os.Exit(1)
    }
    file2, err2 := os.Open(os.Args[2])
    if err2 != nil {
        fmt.Fprint(os.Stderr, err2)
        os.Exit(1)
    }
    paste(bufio.NewScanner(file1), bufio.NewScanner(file2))
    file1.Close()
    file2.Close()
}

main で引数の個数をチェックし、ファイル名が 2 つ指定されていない場合はエラーを表示して終了します。次に、ファイル Args[1] と Args[2] をオープンします。オープンできない場合はエラーを表示して終了します。それから、NewScanner で Scanner を生成して、それらを関数 paste に渡して呼び出します。最後に、Close でオープンしたファイルをクローズします。

関数 paste は簡単です。s1.Scan() で 1 行読み込みます。ファイルが終了した場合は関数 outputFile で s2 を出力してから break で for ループを脱出します。次に、s2 から 1 行読み込みます。ファイルが終了した場合は、s1 の文字列 s1.Text() を出力してから outputFile で s1 を出力します。そうでなければ、文字列 s1 と s2 を連結して出力します。これは Printf を使うと簡単ですね。

関数 outputFile も簡単で、ファイルの最後まで 1 行ずつ読み込んで、それを Println で出力するだけです。これでプログラムは完成です。実際に試して動作を確認してみてください。


初版 2014 年 3 月 23 日
改訂 2021 年 12 月 18 日
改訂 2022 年 1 月 16 日

Copyright (C) 2014-2022 Makoto Hiroi
All rights reserved.

[ PrevPage | Golang | NextPage ]