M.Hiroi's Home Page

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

応用編 : 書式付き入出力関数


Copyright (C) 2022 Makoto Hiroi
All rights reserved.

はじめに

標準ライブラリ fmt に用意されているC言語ライクな書式付き入出力関数は、拙作のページ「ファイル入出力」で簡単に紹介しました。今回はもう少しだけ詳しく書式付き入出力関数について説明します。

●書式付き出力関数

パッケージ fmt の Printf のように、データを整形して出力する関数のことを書式付き出力関数といいます。Printf 以外にも次の関数が用意されています。

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

Printf は標準出力に、Fprintf は io.Writer に整形した結果を出力します。返り値 n は出力したバイト数で、エラーが発生した場合は返り値 err でエラーを報告します。Sprintf は整形した結果を文字列にして返します。

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

主な verb を以下に示します。

%v   デフォルト
%#v  Go 言語の構文表現
%T   データ型の構文表現
%%   記号 % を表す
%t   真偽値 (true, false)
%b, %d, %o, %x
     整数 (2, 10, 8, 16 進数)
%c   文字 (Unicode)
%U   文字 (Unicode 形式, U+16進数)
%e, %f, %g
     浮動小数点数、複素数
%s   文字列
%p   ポインタ

それから、% と変換指示子の間にオプションでいろいろな設定を行うことができます。

  1. フラグ
  2. フィールド幅
  3. 精度
  4. 引数インデックス

これらのオプションは verb によって動作が異なる場合があります。簡単な例を示しましょう。

リスト : 整数の出力 (fmt01.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 fmt01.go
100, 64, 144
[10]
[  10]
[100000]
[123456]
[123456  ]
[00123456]

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

整数値を表示する verb は、データを表示するフィールド幅を指定することができます。最初の例がフィールド幅を指定しない場合で、次の例がフィールド幅を 4 に指定した場合です。10 ではフィールド幅に満たないので、右詰めに出力されています。もし、フィールド幅に収まらない場合は、指定を無視して数値を出力します。フィールド幅を 0 で埋めたい場合は、フラグに 0 を指定します。左詰めにしたい場合は、フラグに - を指定します。

%s は文字列を表示します。%s の場合でも、フィールド幅を指定することができます。簡単な例を示しましょう。

リスト : 文字列の表示 (fmt02.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);
}
$ go run fmt02.go
[hello, world]
[        hello, world]
[hello, world        ]

浮動小数点数や複素数を表示するには %e, %f, %g を使います。

小数点の右側に印字される桁数は、%e と %f ではデフォルトで 6 桁になります。これを変更するには精度を使います。精度はピリオド ( . ) のあとに数字を指定します。たとえば、"%.14f" とすると、小数点は 14 桁で表示されます。%g は最大有効桁数で表示します。%g で精度を指定すると有効桁数が変更されます。小数点数の桁ではないので注意してください。

簡単な例を示します。

リスト : 浮動小数点数の表示 (fmt03.go)

package main

import (
    "fmt"
    "math"
)

func main() {
    pi := math.Pi
    fmt.Printf("%e\n", pi)
    fmt.Printf("%.14e\n", pi)
    fmt.Printf("%f\n", pi)
    fmt.Printf("%.14f\n", pi)
    fmt.Printf("%g\n", pi)
    fmt.Printf("%.6g\n", pi)
}
$ go run fmt03.go
3.141593e+00
3.14159265358979e+00
3.141593
3.14159265358979
3.141592653589793
3.14159

このほかにも、書式付き出力関数にはいろいろな機能があります。詳細は Go 言語のマニュアル「パッケージ fmt」をお読みください。

●Go 言語の文字

Go 言語の文字列は byte の配列 (スライス) として扱われます。Go 言語のプログラム (ソースファイル) は UTF-8 でエンコードすることが規定されているので、文字列の長さはバイト数と文字数で異なる場合があります。当然ですが、文字列に添字を適用すると byte が返されるので、文字単位で値を取り出すことはできません。この場合、for 文の range を使うと文字列から文字を取り出していくことができます。

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

リスト : 文字列から文字を取り出す (char01.go)

package main

import "fmt"

func main() {
    a := "hello, world"
    b := "こんにちは世界"
    fmt.Printf("len(a) = %d\n", len(a))
    fmt.Printf("len(b) = %d\n", len(b))
    for i := 0; i < len(a); i++ {
        fmt.Printf("%x ", a[i])
    }
    fmt.Println("")
    for i := 0; i < len(b); i++ {
        fmt.Printf("%x ", b[i])
    }
    fmt.Println("")
    for i, c := range a {
        fmt.Printf("%d: [%c]\n", i, c)
    }
    fmt.Println("")
    for i, c := range b {
        fmt.Printf("%d: [%c]\n", i, c)
    }
    fmt.Println("")
}
$ go run char01.go
len(a) = 12
len(b) = 21
68 65 6c 6c 6f 2c 20 77 6f 72 6c 64
e3 81 93 e3 82 93 e3 81 ab e3 81 a1 e3 81 af e4 b8 96 e7 95 8c
0: [h]
1: [e]
2: [l]
3: [l]
4: [o]
5: [,]
6: [ ]
7: [w]
8: [o]
9: [r]
10: [l]
11: [d]

0: [こ]
3: [ん]
6: [に]
9: [ち]
12: [は]
15: [世]
18: [界]

Go 言語の場合、文字を表すデータ型として rune が用意されています。rune は int32 の別名で、文字を表す Unicode の値になります。文字リテラルは 'a' や 'あ' のように ' で囲んで記述します。文字数は標準ライブラリ unicode/utf8 の関数 RuneCountInString で数えることができます。

func RuneCountInString(s string) (n int)

ファイルなどから文字単位でデータを読み込みたい場合は、標準ライブラリ bufio の関数 ReadRune を使うと便利です。

func (b *Reader) ReadRune() (r rune, size int, err error)

返り値 r が文字、size がバイト数、err がエラーを表します。

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

リスト : 文字単位での読み込み (char02.go)

package main

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

func main() {
    rd := bufio.NewReader(os.Stdin)
    for {
        r, s, err := rd.ReadRune()
        if s == 0 {
            if err != io.EOF {
                fmt.Printf("oops! %v\n", err)
            }
            break
        } else {
            fmt.Printf("%c\n", r)
        }
    }
}
$ go run char02.go
hello, world
h
e
l
l
o
,

w
o
r
l
d


こんにちは世界
こ
ん
に
ち
は
世
界


●書式付き入力関数

ファイルからデータを読み込む場合、データをバッファに格納するだけではなく、数値や他のデータ型に変換できると便利です。Go 言語の場合、書式付き入力関数を使うと、入力データの変換を行うことができます。

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

Scanf は標準入力 (os.Stdin)、Fscanf は io.Reader (r)、Sscanf は文字列 (str) からデータを読み込みます。返り値 n は読み込んだデータ数で、失敗した場合は返り値 err でエラーを報告します。書式文字列 format のあとの引数には、読み込んだデータを格納する変数をポインタで指定します。

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

リスト : Sscanf の使用例 (fmt04.go)

package main

import "fmt"

func main() {
    var n int
    var d float64
    var s string
    fmt.Sscanf("5678abcd", "%d", &n)
    fmt.Printf("%d\n", n)
    fmt.Sscanf("5678abcd", "%o", &n)
    fmt.Printf("%o\n", n)
    fmt.Sscanf("5678abcd", "%x", &n)
    fmt.Printf("%x\n", n)
    fmt.Sscanf("1.23456789", "%f", &d)
    fmt.Printf("%f\n", d)
    fmt.Sscanf("hello, world", "%s", &s)
    fmt.Printf("%s\n", s)
}
$ go run fmt04.go
5678
567
5678abcd
1.234568
hello,

%d はデータを 10 進整数に変換します。入力データが 5678abcd の場合、5678 まで読み込んで整数に変換します。このとき、入力データ abcd は残っていることに注意してください、%o は 8 進整数に、%x は 16 進整数に変換します。%f はデータを浮動小数点数 (float64) に変換します。%s は空白以外の文字列を読み込みます。空白文字 (タブや改行も含む) は区切り記号として使われます。

書式文字列の中では複数の verb を指定することができます。入力データの空白文字は区切り記号として扱われるので、"%d%d%d" は入力データ "123 456 789" とマッチングします。なお、先頭にある空白文字は読み飛ばされますが、読み飛ばさない変換指示子 (たとえば %c など) もあります。

●Scanf の簡単な使用例

それでは簡単な使用例として、ファイルに数値データがありそれを全部足し算するプログラムを作ってみましょう。次のリストを見てください。

リスト : 数値データの総和 (fmt05.go)

package main

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

func main() {
    a, c, n := 0, 0, 0
    for {
        m, err := fmt.Scanf("%d", &n)
        if m == 0 {
            if err == io.EOF {
                fmt.Printf("入力数 %d, 合計 = %d\n", c, a)
                break
            } else {
                fmt.Printf("%d 番目のデータに誤りがあります: %v\n", c + 1, err)
                os.Exit(1)
            }
        }
        a += n
        c++
    }
}

書式文字列には区切り文字が指定されていませんが、先頭の空白文字は読み飛ばされるので問題ありません。ファイルが終了した場合、Scanf は 0 と io.EOF を返します。m と err をチェックしてファイルが終了したならば、入力されたデータ数 (変数 c) と合計値 (変数 a) を Printf で表示します。

err が io.EOF でない場合、データの読み込みに失敗しています。Printf でデータの番号とエラーの種類を表示して処理を終了します。それ以外の場合はデータを正常に読み込むことができました。a に n の値を加算して c の値をインクリメントします。

簡単な実行例を示します。

$ cat input.txt
 28236
 21625
 22672
 30646
  8110
 30976
$ go run fmt05.go < input.txt
入力数 6, 合計 = 142265
$ cat inputerr.txt
 28236
 21625
 22672
 30646
 abcde
  8110
 30976
$ go run fmt05.go < inputerr.txt
5 番目のデータに誤りがあります: expected integer
exit status 1

●単語のカウント

最後に簡単な例題として、ファイルのバイト数と行数と単語をカウントするプログラムを作ります。単語は空白文字で区切られた文字列とします。なお、Unix 系 OS には同等の機能を持つコマンド wc があります。次のリストを見てください。

リスト : 単語のカウント

package main

import (
    "os"
    "io"
    "bufio"
    "fmt"
    "unicode"
)

func wc(file *bufio.Reader) (int, int, int) {
    b, l, w := 0, 0, 0    // バイト数, 行数, 単語数
    inword  := false
    for {
        c, s, err := file.ReadRune()
        if err == io.EOF { return b, l, w }
        if c == unicode.ReplacementChar {
            fmt.Fprintf(os.Stderr, "%d バイト目で不正なコードを検出しました\n", b + 1)
            os.Exit(1)
        } else if unicode.IsSpace(c) {
            inword = false
            if c == '\n' { l++ }
        } else if !inword {
            inword = true
            w++
        }
        b += s
    }
}

func main() {
    b, l, w := wc(bufio.NewReader(os.Stdin))
    fmt.Printf("%d %d %d\n", l, w, b)
}

ファイルは標準入力 (os.Stdin) から読み込みます。実際の処理は関数 wc で行います。変数 b, l, c はそれぞれバイト数、行数、単語数をカウントします。文字の読み込みは bufio のメソッド ReadRune を使います。

func (b *Reader) ReadRune() (r rune, size int, err error)

返り値は文字、バイト数、エラーです。文字が正しくない場合、U+FFFD と読み込んだバイト数 1 を返します。U+FFFD は標準ライブラリ unicode に定数 ReplacementChar として定義されています。空白文字は関数 unicode.IsSpace で判定することができます。これは ASCII コードだけではなく Unicode の空白文字もチェックしてくれます。unicode には文字種別を判定する関数が用意されています。

文字 c が空白文字であれば変数 inword を false にセットします。そして、inword が false で c が空白文字以外であれば、inword を true にセットして変数 w を +1 します。これで単語をカウントすることができます。行数は c が空白文字かつ改行文字 '\n' であれば変数 l の値を +1 します。バイト数は ReadRune の返り値 s を変数 b に加算するだけです。

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

$ go run wc.go
hello, world
こんにちは 世界 さようなら
<- CTRL-D を入力
2 5 56
$ go run wc.go < wc.go
38 109 643
$ wc < wc.go
 38 109 643

正常に動作しているようですね。

ご参考までに、bufio.Scanner を使ったプログラムを示します。

リスト : 単語のカウント (2)

package main

import (
    "os"
    "bufio"
    "fmt"
    "unicode"
    "unicode/utf8"
)

func wc(s *bufio.Scanner) (int, int, int) {
    b, l, w := 0, 0, 0    // byte 数, line 数, word 数
    inword  := false
    s.Split(bufio.ScanRunes)
    for s.Scan() {
        c, s := utf8.DecodeRune(s.Bytes())
        if c == utf8.RuneError {
            fmt.Fprintf(os.Stderr, "%d バイト目で不正なコードを検出しました\n", b + 1)
            os.Exit(1)
        } else if unicode.IsSpace(c) {
            inword = false
            if c == '\n' { l++ }
        } else if !inword {
            inword = true
            w++
        }
        b += s
    }
    return b, l, w
}

func main() {
    b, l, w := wc(bufio.NewScanner(os.Stdin))
    fmt.Printf("%d %d %d\n", l, w, b)
}

関数 Split で分割関数を ScanRunes に設定します。分割関数は Scan を実行する前に設定してください。トークンは文字列またはバイト列でしか取り出すことができません。バイト列を文字に変換するには標準ライブラリ unicode/utf8 の関数 DecodeRune を使います。

func DecodeRune(p []byte) (r rune, size int)

DecodeRune は引数のバイト列 p の先頭から 1 文字取り出します。返り値 r が取り出した文字、size がバイト数です。文字が正しくない場合、文字 utf8.RuneError (U+FFFD) を返します。あとのプログラムは同じなので説明は割愛させていただます。興味のある方は実際に動かしてみてください。

●参考 URL

  1. パッケージ fmt - Go 言語

初版 2022 年 1 月 9 日