Top▲
マーリンアームズ サポート   翻訳   コンサル   講座   アプリ   コラム

初めてのGo言語 —— 他言語プログラマーのためのイディオマティックGo実践ガイド    Jon Bodner著    武舎広幸訳

付録B 実例で学ぶGo言語入門

武舎 広幸

ほかの言語(特にCやC++)をある程度ご存じの方のための、サンプルプログラム集です。原著の最初のほうの章には、まとまったコードがあまり書かれていないので、以下に示す例題をざっと読んでおくと、Goの世界に馴染んで、本文の内容がわかりやすくなると思います。

訳者が原著を始めとする本などの情報を元に、これまでの経験から、できるだけわかりやすいと思う方法で書いてみました。タイムマシンで半年前に戻って、訳者自身がこれを読んだら「おかげで随分楽ができる」と思える内容にしたつもりです。

B.1 予備知識

まず、サンプルコードを読むための予備知識です。

B.1.1 Go言語のコードの留意点

下のコードを読んだり試したりする上で、とくに次の点に留意してください。なお、「付録A Go言語のまとめ」には、より詳しい説明があります。さらに詳しくは、索引や目次などを頼りに本文を参照してください。

func main() {

 次のように書いてしまうとコンパイル時のエラーになる。

func main()
{

B.2 fmtパッケージの動詞(verb)

これから見る例にも何度か登場するfmt.Printffmt.Scanfなどを含むパッケージfmtは、入出力やフォーマット(書式)に関する機能を提供してくれます(C言語のprintfなどやscanfなどと似ています)。

下のコードで使われているfmt.Printfなど、最後にfが付いているfmtの関数には第1引数に文字列を指定して出力や入力のフォーマットを指定します。このときに%d%sなどのverb(動詞)が使われます。

fmt.Printf("第%d問の答えは%sです\n", i, answer)  // 標準出力(通常は画面)に書く

原著にverbに関する記載がほとんどないので、最初の例題として、verbのうち、よく使われるものや、便利と思われるものを紹介します。例題ディレクトリ(フォルダ)の下のch22b/00fmt-verbにコードを置きますので、次の手順を参考に実行してみてください。

B.2.1 例題ディレクトリの構成

ディレクトリch22b/00fmt-verbには次の2つのファイルが入っています。

B.2.2 プログラムの実行

実行するには次のコマンドを入力してください(詳しくは1章参照)。

$ cd ch22b/00fmt-verb   例題ディレクトリの00fmt-verbに移動
$ go run main.go 

あるいは次のようにしても大丈夫です。go buildでコンパイル後fmt-verbという実行形式のファイルができるので、それを実行します(コンパイラは同じディレクトリにあるファイルgo.modを参照してfmt-verbというファイル名を決めます)。

$ cd ch22b/00fmt-verb 
$ go build 
$ ./fmt-verb 

なお、最初の方法(go run main.goを実行)では、一時テンポラリファイルに実行形式ができてそれが実行され、終了すると削除されます。例をちょっと試すといった場合には、最初の方法が便利です。

また、go build -o xxxのようにしてもxxxという実行形式のファイルができますので、./xxxでコマンドを実行できます(go.modの指定より優先されます)。

go buildコマンドを使うと、ほかの環境で動作する実行ファイルを(とても)簡単に作ることができます。たとえば、次のコマンドでLinux(CPU: AMD64)用の実行ファイルを作成できます。ファイル名はfmt-verbのママになります。

$ GOOS=linux GOARCH=amd64 go build 

また、次のコマンドでWindowsのコマンド(ファイル名fmt-verb-windows.exe)が作成され、Widowsマシンにコピーして実行することができます。

$ GOOS=windows GOARCH=386 go build -o fmt-verb-windows.exe main.go 

逆に、たとえばWindowsのPowerShellで次を実行するとmacOS(Intelプロセッサ用)の実行ファイルができます(Appleシリコン用の場合はamd64の代わりにarm64を指定します。なお、Intelプロセッサ用の実行ファイルはAppleシリコンでも動作しますので、実行速度が問題でなければamd64を指定しておいたほうが安全です)。

  $Env:GOOS = "darwin"; $Env:GOARCH = "amd64"; go build -o fmt-verb-mac
 

Macにコピーして、実行パーミッションを付加すれば(chmod +x fmt-verb-mac)実行できるようになります。

詳しくはhttps://musha.com/scgo?ln=ax04https://musha.com/scgo?ln=ax06などを参照してください。

B.2.3 実行結果

上のいずれかの方法でfmt-verb実行してみると、次のような結果が表示されるはずです(行頭の行番号は説明のためのもので、実行結果では表示されません)。

 1: 第3問の答えは「Go言語」です。
 2: 文字列
 3: バッククオートを使った、改行や
 4:         タブが入った
 5:                 文字列
 6: "バッククオートを使った、改行や\n\tタブが入った\n\t\t文字列"
 7: d: 254  b: 11111110  o: 376  x: fe  X: FE
 8: f: 3.141593  F: 3.141593
 9: e: 3.141593e+00  E: 3.141593E+00
10: v: 文字列  254  3.14159265358
11: T: string  int  float64
12: 「%%」で「%」をひとつ出力
13: %fでs1を指定: %!f(string=文字列)

ではソースコードを見ましょう。main.goのソースコードを示します(ch22b/00fmt-verb/main.go)。コード中の「←1:」などは実行結果の行番号との対応を示しています。

package main    // メインプログラムは必ずこうする

import "fmt"    // 読み込むパッケージ(ライブラリ)の宣言

func main() {   // 関数の定義。必ずmainが必要。「{」の前で改行してはダメ!
    i := 3      // 変数iに3が代入される。型は自動判定されるこの場合はint(整数)
    answer := "Go言語"  // "Go言語"は文字列なので、answerの型はstring(文字列)
    fmt.Printf("第%d問の答えは「%s」です。\n", i, answer)     // ←1:
    // %dがiに、%sがanswerに対応

    s1 := "文字列"
    fmt.Printf("%s\n", s1)                                  // ←2:
    s2 := `バッククオートを使った、改行や
    タブが入った
        文字列`
    fmt.Printf("%s\n", s2)  // そのまま表示                   // ←3〜5:
    fmt.Printf("%q\n", s2)  // 「エスケープ」される            // ←6:

    i = 254  // 上でiの型はintになっているので、254は代入できる
    fmt.Printf("d: %d  b: %b  o: %o  x: %x  X: %X\n", i, i, i, i, i)  // ←7:

    f := 3.14159265358      // 小数(型はfloat64)
    fmt.Printf("f: %f  F: %F\ne: %e  E: %E\n", f, f, f, f)  // ←8〜9:
    fmt.Printf("v: %v  %v  %v\n", s1, i, f)                 // ←10:
    fmt.Printf("T: %T  %T  %T\n", s1, i, f)                 // ←11:
    fmt.Printf("「%%%%」で「%%」をひとつ出力\n")               // ←12:

    fmt.Printf("%%fでs1を指定: %f\n", s1)     // ←13:(間違いあり!)
}

import "fmt"でパッケージ(ライブラリ)fmtを読み込んで利用できるようにしています。これ以降のコードでfmt.があるものはfmtパッケージで定義されたものになります。また、パッケージの外から使える関数等は大文字で始める約束になっているので、Printfと「P」が大文字になっています。importしたパッケージの関数名は必ず大文字で始まります。

このサンプルではverbのうち、よく使われそうなもの、知っていると便利なものを使っています。

%v関連で、もう少し詳しい情報を出力してくれるものがあります。ここでは例を示しませんが、覚えておくとデバッグのときなどに便利かと思います。

なお、実行結果の13行目のように、verbに対応する変数などがない場合や、対応する変数などの型が指定とあっていない場合は、%!fなどのように「!」が表示されますので、変数などとverbの対応を確認し修正してください。

このほか、パッケージfmtについての詳細は次のページなどを参照してください。

B.3 基本構文と標準入出力

次はGo言語の基本的な構文を紹介します。1以上10以下の数字を当てるゲームです。実行してみながらお読みください(「go run main.go」あるいは「go build」の後で「./guessnum1」)。

では実際のコードを見ていきましょう。main.goのソースコードを示します(ch22b/01guessnum1/main.go)。

package main  // メインプログラムは必ずこうする

import (  // 読み込むパッケージ(ライブラリ)の宣言
    "fmt"   // 入出力関連のパッケージ 。Scanln と Printlnを使う
    "strconv"  // 文字列から整数などへの変換。Atoiを使う
) // 複数のパッケージを呼び込む場合は、(...)で指定すると「import」は1個で済む

func main() {
    answer := 4  // :=を使うと型は自動判別してくれる。この場合answerの型はint
    fmt.Print("数当てゲームです。1以上10以下の整数を(半角で)入力してください: ")
    // パッケージfmtの関数Printを使う。パッケージ外に公開する関数は必ず大文字で始まる
    var inp string  // 「var 変数 型」の順。変数には型の「ゼロ値」が入る(この場合は空文字列)
    fmt.Scanln(&inp) // 文字列として読み込み。&はポインタを表す
    // inpの値を書き換えてもらうためにポインタを渡す。詳しくは6章参照
    // Scanlnは読み込んだ単語数(スペースあるいは改行区切り)とエラーを戻すが
    // 今回はどちらも無視するので、結果を変数に代入していない
    // 下で整数に変換できなければエラーになるのでこれで大丈夫
    num, err := strconv.Atoi(inp) // 整数に変換
    if err != nil || num < 1 || 10 < num  {    // 条件を(...)でくくらない
        fmt.Println("1以上10以下の整数ではないので、ハズレです。")
    } else if num == answer { // else を「}」と同じ行に*必ず*書くことに注意
        fmt.Println("ビンゴ!")
    } else {  // else を「}」と同じ行に*必ず*書くことに注意
        fmt.Println("残念でした。ハズレです。")
    }
}

B.3.1 例外処理

コメントで説明を書いておきましたが、「例外処理」の説明を加えましょう。

Go言語に例外処理はありません。関数から複数の値を返すことができ、返す複数の値の最後にerror型を指定することでエラーを処理するのが一般的です。

上の例では、文字列(「半角」の数字列)を整数に変換する関数strconv.Atoiを呼び出すと、2つの値が戻ります(if文の上)。numは変換結果の整数ですが、errがエラーが起こったかどうかを示すerror型の変数です。変数errnilでないならばエラーが起こったことになります。その場合(および入力された値が1より小さかったり10より大きかった場合)をif文で判定しています。

B.3.2 関数とループ

次の例はループや関数を使って少し複雑にしています(ch22b/02guessnum2/main.go)。この例も実行してみながらコードをお読みください。

package main  // メインプログラムは必ずこうする

import (  // 読み込むライブラリの宣言
    "fmt"   // 出力関連
    "strconv"  // 文字列から整数などへの変換
    "math/rand"   // 乱数。強力なものが必要ならcrypto/randを使う
    "time"  // 乱数のseed(種)を生成するため
)

// main
func main() {
    answer := getTheNumber();    // 関数呼び出し。答えの数字をもらう

    for count:=1; ; count++ {  //「条件」を指定しないと無限ループになる
        printPrompt(count) // 説明のメッセージを表示

        if num, err := readUserAnswer();
        err != nil || num < 1 || 10 < num  {
            // 条件の前で変数を宣言し、値の代入もできる。err以降がifの条件
            fmt.Println("1以上10以下の整数ではないので、ハズレです。")
            // fmt.Printlnは改行する
        } else if num != answer {
            fmt.Println("残念でした。ハズレです。")
        } else {
            printSuccessMessage(count) // あたったときのメッセージを表示
            break  // forループを抜ける
        }
    }
}

// getTheNumber 当てる数字を得る
func getTheNumber() int { // 関数宣言。引数はなし、戻り値はint(整数)
    rand.Seed(time.Now().UnixNano()) //乱数のseed。’70年1月1日0時からのナノ秒数
    num := rand.Intn(10)+1  // 0以上10未満の整数をもらって、+1する
    // fmt.Println("答えは: ", num)
    return num
}

// printPrompt プロンプト文字列(説明)を表示
func printPrompt(count int) { // 引数は整数。戻り値はなし
    if count == 1 { // 1回目のみ表示
        fmt.Print("数当てゲームです。")  // fmt.Printは改行しない
    }
    fmt.Printf("1以上10以下の整数を(半角で)入力してください(%v回目): ", count)
    // fmt.Printfではフォーマットが指定できる。%vを指定すると「良きに計らって」くれる
}

// readUserAnswer ユーザーからの答えを読み込む
func readUserAnswer() (int, error) {  // 戻り値が2つある
    var inp string
    fmt.Scanln(&inp) // 文字列として1行読み込み。詳細は前の例のScanlnのところ参照
    return strconv.Atoi(inp) // 整数に変換。errorも戻る
}

// printSuccessMessage 当たったときのメッセージを表示
func printSuccessMessage(count int) {
    if count == 1 {
        fmt.Printf("ビンゴ! おめでとうございます。一発であたりましたね。素晴らしい!\n")
    } else {
        adverb :="" // 副詞(修飾語「ヤット」をつけるかどうか)
        if  7 < count  {
            adverb = "ヤット"
        }
        fmt.Printf("おめでとうございます。%v回目で%sあたりましたね。\n", count, adverb)
    }
}

B.3.3 switch

Goのswitch文では、caseで条件式を書けます。上の例のif文をswitch文に書き換えたものをch22b/03guessnum-switch/main.goに置きました。switch文が登場する2つの関数を下に示します。

関数main(switch文を使ったもの)

func main() {
    answer := getTheNumber();
loop:  // 下のbreakでループを抜けるための「ラベル」
    for count := 1; ; count++ {  // 無限ループ
        printPrompt(count) // 入力を促すメッセージを表示

        switch num, err := readUserAnswer(); { // 答えを読み込む
        case err != nil || num < 1 || 10 < num:
            fmt.Println("1以上10以下の整数ではないので、ハズレです。")    // fmt.Printlnは改行する
            // breakがなくても、このcaseはここで終了
        case num != answer:
            fmt.Println("残念でした。ハズレです。")
            // breakがなくても、このcaseはここで終了
        default:
            printSuccessMessage(count) // 当たったときのメッセージを表示
            break loop // forループを抜ける。loopは「ラベル」。breakだけだとdefaultを抜けるだけになってしまう
        }
    }
}

関数printSuccessMessage(switch文を使ったもの)

func printSuccessMessage(count int) {
    adverb := ""
    switch {
    case count == 1:
        fmt.Printf("ビンゴ! おめでとうございます。一発であたりましたね。素晴らしい!\n")
    case 7 < count:
        adverb = "ヤット"
        fallthrough  // この下のcaseも実行。「落っこちる」
    default:
        fmt.Printf("おめでとうございます。%v回目で%sあたりましたね。\n", count, adverb)
    }
}

B.3.4 日本語の識別子

Go言語では日本語の識別子(変数名や関数名など)も使えます(詳細は「2.6 変数および定数の名前」)。ただし、次のように、英語(アルファベット)で記述することを前提としていると思える要素もあるので注意が必要です。

上の2つの関数を日本語の識別子を使って書き換えたものを下にリストします(全体はch22b/04guessnum-jpn/main.go)。

関数main(日本語の識別子を使ったもの)

func main() {
    正解 := 当てる数字を決定();
loop:
    for カウンタ := 1; ; カウンタ++ {  // 無限ループ
        プロンプトを表示(カウンタ) // 入力を促すメッセージを表示

        switch 解答, err := ユーザーからの答えを取得(カウンタ); { // 答えを読み込む
        case err != nil || 解答 < 1 || 10 < 解答:
            fmt.Println("1以上10以下の整数ではないので、ハズレです。")
            // breakがなくても、このcaseはここで終了
        case 解答 != 正解:
            fmt.Println("残念でした。ハズレです。")
            // breakがなくても、このcaseはここで終了
        default:
            成功時のメッセージを表示(カウンタ) // 当たったときのメッセージを表示
            break loop // forループを抜ける。breakだけだとdefaultを抜けるだけになる
        }
    }
}

関数printSuccessMessageの日本語の識別子を使ったバージョン

func 成功時のメッセージを表示(カウンタ int) {
    修飾語 := ""
    switch {
    case カウンタ == 1:
        fmt.Printf("ビンゴ! おめでとうございます。一発であたりましたね。素晴らしい!\n")
    case 7 < カウンタ:
        修飾語 = "ヤット"
        fallthrough  // この下のcaseも実行。「落っこちる」
    default:
        fmt.Printf("おめでとうございます。%v回目で%sあたりましたね。\n", カウンタ, 修飾語)
    }
}

B.4 コマンド行計算機——コマンド行引数、文字列の置換と正規表現、外部コマンド

今度はコマンドライン引数の処理と、文字列の置換を行う例を見てみましょう。さらには外部コマンドを呼び出す方法も説明します。

Unix系OSには(古くから)bc(basic calculator)というコマンドがあってGUIの電卓(アプリ)を起動しなくてもCUIで簡単に計算ができます*1。ファイルに式を書いておいて一度に実行したり、コマンドの出力を入力として計算したりもできます。

[*1] Windows(一部のLinuxも?)には標準ではbcshがありませんので、Goの範囲内で式の評価を行ってしまうバージョンを作っておきました。ch22b/05b-calc-evalの下を参照してください。なお、Windowsでもbcを動かすことができます。次のページなどを参照してください——https://musha.com/scgo?ln=ax01

たとえば次のような感じで使えます(引数-l——小文字のLエル——を付けないと結果が整数になります)。

$ bc -l 
3400*2             3400×2を計算
6800               答えは 6800
(3330+95-120)*1.1  (3330+95-120)×1.1
3635.5             上の答え
12^3               12の3乗(12×12×12)
1728               上の答え

訳者もbcをよく使うのですが、bcではいわゆる「全角」の数字や演算子が入っていたり、数字に3桁ごとの区切りの「,」が入っていたりするとエラーになってしまい、ウェブページから数値をコピーして計算したりするには(特に日本人にとっては)ちょっと不便な面もあります。そこで次のように改良(?)したコマンドを作ることにしましょう。

このプロジェクトはch22b/05a-calc-bcの下にあり、ソースコードは2つのファイル(main.gocalculate.go)に分かれています。たとえば次の手順で実行してみてください。

$ cd ch22b/05a-calc-bc       
$ rm -f calc go.mod go.sum   ソースコード以外のものを(あれば)消す
$ go mod init example/calc   go.modファイルができる
$ go mod tidy                必要なモジュールをダウンロードし、go.sumファイルができる
$ go build                   ビルド。実行形式ファイルcalcが作られる
$ ./calc "3、333*1.1"   引数に計算式を全角で指定
次の計算をします: 3333*1.1     文字列を変えたので、一応確認
3666.3                       答え
12^5                      12の5乗(12×12×12×12×12)
次の計算をします: 12^5
248832
345,123÷12 
次の計算をします: 345123/12
28760.25000000000000000000
...                          いろいろ計算してみてください
                             改行のみで終了

コードに細かくコメントを書いてありますので詳細は省略しますが、次のような点に留意してください。

ファイルch22b/05a-calc-bc/main.goの内容は次のとおりです。

package main

import (
    "fmt"
    "os" // コマンドライン引数の処理
    "strings"  // 文字列置換
    "regexp"  // 正規表現
    "bufio" // 読み込み
    "golang.org/x/text/width" // 全角 -> 半角変換
)

func main() {
    var exp string  // EXPression: 式
    if n := len(os.Args); 2 <= n { // 引数があったら(len()でサイズがわかる)
        for i := 1; i < n; i++ {
            exp += os.Args[i] // 引数を後ろに追加していく
        }
        calculateAndPrintValue(exp) // 必要な変換を行って計算する
    }

    scanner := bufio.NewScanner(os.Stdin) // 標準入力を受け付ける「スキャナ」
    for scanner.Scan() { // 1行分の入力を取得できる限り繰り返す
        exp = scanner.Text()  // 入力のテキスト(string)を取得
        if exp == "" {
            return  // 空行入力で終了
        }
        calculateAndPrintValue(exp) // 必要な変換を行って計算する
    }
}

// calculateAndPrintValue  文字の置換を行って計算し出力する
func calculateAndPrintValue(exp string) error {
    expOrigin := exp
    exp = replaceChars(exp) // ÷->/  全角->半角変換などを行う
    if (exp != expOrigin) {
        fmt.Println("次の計算をします:", exp) // 変えた時は確認のため表示
    }
    result, err := calculate(exp) // 計算実行。エラーが起こればerrがnilでなくなる
    if err != nil || len(result)==0 { // エラーが起こったか、結果が空の時
        fmt.Println("計算できません")
    } else {
        fmt.Printf("%s\n", result)
    }
    return err
}

// replaceChars  置換する
func replaceChars(exp string) string {
    exp = width.Fold.String(exp)  // 全角 -> 半角
    re := regexp.MustCompile(`[xX]`)
    exp = re.ReplaceAllString(exp, "*") // 「x」「X」 -> 「*」

    re = regexp.MustCompile(`[,、,]`) // 3桁ごとの区切り
    exp = re.ReplaceAllString(exp, "") // 「,」「、」「,」 削除

    re = regexp.MustCompile(`[。.]`)
    exp = re.ReplaceAllString(exp, ".") //全角の 「.」「。」 -> 半角の「.」

    exp = strings.ReplaceAll(exp, "÷", "/") //  「÷」 -> 「/」
    return exp
}

もうひとつのファイルcalculate.goの内容は次のとおりです。

package main

import (
    "fmt"
    "os/exec" // OSコマンドの実行
    "strings"
)
// calculate  bcコマンドを呼び出して計算する
func calculate(exp string) (string, error) {
    command := fmt.Sprintf("echo \"%s\" | bc -l", exp)
    // コマンドの文字列を作る。Sprintfの%sの部分がexpの値で置き換わる
    result, err := exec.Command("sh", "-c", command).Output()
    // resultはバイト列(型byteのスライス -- []byte)
    // スライスは「可変長の配列」。Goの配列はサイズ固定だがスライスは変更可
    // err はエラーがあるかどうかを示す。errが nilならば正常終了
    // err の処理は呼び出し側(calculateAndPrintValue)が行う
    if err != nil {
        return "", err
    }
    r := string(result)
    r = strings.TrimRight(r, "\n") // 最後に改行がついて戻るので削除する
    return r, err // 2つの値を戻す
}

ビルドしてできたファイルcalcを実行パス(コマンド検索パス)にコピーしておけば、いつでも実行できるようになります。

B.5 ファイルの入出力

ファイルからの読み込みやファイルへの書き込みにはいくつかの方法が用意されています。

訳者は翻訳ソフトなどで使う辞書ファイルの形式変換によくPerlのスクリプトを使っているのですが、OSのバージョンアップでPerlのバージョンが変わって、「これまでのスクリプトが(前と同じようには)動かなくなってしまった」ということが何度かありました。また、ファイルが大きくなってくると、速度も少し気になります。そこで、こうした処理のいくつかをGo言語に置き換えてみました。

ここでは2つの例を紹介します。

B.5.1 ファイルの内容を一度に読み込み

最近のパソコンは(昔に比べると)メモリ容量も大きいので、巨大なファイルでなければ、ファイル全体を読み込んで一度に処理してしまうと話が単純になります。

単純な例題ですが、ファイル中の表現の一斉置換をしてみましょう。次のような「辞書」のファイルがあるとします。

// 「//」で始まる行はコメント。英語と日本語はタブで区切る
fungible        代替可能な, 代替性のある, 代替物
fungibility     代替可能性
// fungibility  代替できること
pronunciation   発音

この辞書ファイルの形式が他の会社の辞書の形式と違っていたので、形式を揃えてほしいという要望が来ました。

たとえば、次のコードでこの変換ができます(ch22b/07filea-wholeatonce/main.go)。

package main

import (
    "fmt"
    "os" // ファイルの読み込みなど
    "strings" // 文字列置換
    "log"  // ログを書くのに便利。
)

// 定数の宣言(詳細は「2.3 定数」)。型は右辺のデフォルト(この場合string)になる
const path1 = "testdata/dict.txt"

func main(){
    allData, err := os.ReadFile(path1) // 全データを一度に読み込み
    // allDataは[]uint8(8ビット符号なし整数のスライス)
    // io/ioutilにもReadFileがある。現在も動作するが、今は単に上のos.ReadFileが呼ばれる
    if err != nil {
        log.Fatal(err) // エラーをログに書いて異常終了
    }
    s := string(allData)     // string(allData)で型をstringに変換してから呼び出し
    s = strings.ReplaceAll(s, "//", "##") // 「//」を「##」にすべて置換
    s = strings.ReplaceAll(s, "\t", "|")  // 「タブ」を「|」にすべて置換
    fmt.Printf("%s", s) // 文字列置換した結果を出力
}

実行してみてください。次のような結果が表示されるはずです。

$ go run main.go 
## 「##」で始まる行はコメント。英語と日本語はタブで区切る
fungible|代替可能な, 代替性のある, 代替物
fungibility|代替可能性
## fungibility|代替できること
pronunciation|発音

ファイル名path1の値を存在しないファイル名(たとえばtestdata/xxx.txt)に変更して実行すると次のようなエラーメッセージが表示されることになります。

2022/07/04 15:45:21 open testdata/xxx.txt: no such file or directory
exit status 1

上のプログラムではファイルの内容をすべて文字列(string)に変換してからstrings.ReplaceAllを読んでいますが、bytes.ReplaceAllを使うとバイト列([]byte)のまま置換することができます。そちらを使ったバージョンをch22b/07filea2-wholeatonce-bytes/main.goに置きました。

変更部分は次のとおりです。

allData = bytes.ReplaceAll(allData, []byte( "//"), []byte("##"))
// 第2、第3引数はバイト列に変換してから呼び出し
allData = bytes.ReplaceAll(allData, []byte( "\t"), []byte("|"))
fmt.Printf("%s", allData) // 置換した結果を出力。%sを指定すれば文字列になる

string(文字列)として扱っておいたほうが「安全」に感じますが、何度も置換操作を行うようなら、バイト列([]byte)として処理したほうが効率がよさそうです。

なお、strings.NewReplacerを使うと、次のコードのように複数のペアを指定して一度に置換ができます(ch22b/07filea3-wholeatonce-new-replacer/main.go)。

r := strings.NewReplacer("//", "##", "\t", "|") // 一度に複数のペアを指定できる
result := r.Replace(string(allData)) // replacerを使って文字列を置換
fmt.Printf("%s", result)

B.5.2 ファイルを1行ずつ処理

通常はファイルの内容を1行ずつ処理をしていくほうがメモリの使用量も少なくなり、ファイルが巨大になっても安全です。

今度の例は、まず実行結果をお見せしましょう。

$ cd ch22b/07fileb-linebyline 
$ cat testdata/dict2.txt 
## #で始まる行はコメント
fungible|代替可能な, 代替性のある、 代替物
fungibility|代替可能性
# fungibility|代替できること
pronunciation|発音,発音記号

$ go run main.go 
fungible|代替可能な
fungible|代替性のある
fungible|代替物
fungibility|代替可能性
pronunciation|発音
pronunciation|発音記号

この辞書ファイルdict2.txtではカラムを「|」(パイプ記号)で区切って、左側(第1カラム)に英単語、右側(第2カラム)にその訳語を書いています。第2カラムには複数の訳語を書くことができ、「,」(半角あるいは全角)あるいは「」で区切られています。変換後は訳語を「展開」して、1行に英単語とその訳語を1個ずつ書くようにします。また、コメント行は削除します。

次のようなコードを書いてみました(ch22b/07fileb-linebyline/main.go)。

package main

import (
    "fmt"
    "os"
    "bufio"
    "strings"
    "regexp"
)

var re1 = regexp.MustCompile(`[,,、] *`) // 正規表現をコンパイルしておく

func main(){
    data, _ := os.Open("testdata/dict2.txt")
    defer data.Close()

    scanner := bufio.NewScanner(data)
    for scanner.Scan() {
        s := scanner.Text()
        if s == "" || s[0:1] == "#" { // s[0:1] で先頭の1文字
            continue  // この行はスキップ(コメントあるいは空白行)
        }
        columns := strings.Split(s, "|") // "|"で分割
        if len(columns)!=2 { // カラム数が2個でないとき(len()でサイズがわかる)
            continue // この行はスキップ。形式がおかしい
        }
        eng := columns[0] // 英語部分
        jpn := columns[1] // 日本語訳
        translations := re1.Split(jpn, -1) // 正規表現を使って分割。-1は全部
        // []string(文字列のスライス)が戻る
        for _, translation := range translations {
            // range からはインデックス(添字)と文字列が戻るが、インデックスは無視する
            // 「_」に代入すると無視できる
            // 変数に代入するとその変数を使わないとエラーになってしまうので
            fmt.Printf("%s|%s\n", eng, translation);
        }
    }
}

実行速度が問題になるケースでは、go run main.goで実行するよりも、前もってgo buildなどでコンパイルしておいてコマンドを実行したほうが当然処理は速くなります。正規表現による文字列の処理(マッチングや置換)は、概して単純な文字列の処理に比べると時間がかかるので、速度が問題になる課題の場合は、注意が必要です(見通しがよいと改善もしやすいので、わかりやすさも大切ですが)。

なお、ch22b/07filec-linebyline-regexpには正規表現のマッチング部分を変数(スライス)に記憶して、以降の処理に利用して変換するバージョン(機能は同じ)があります。

ここまでファイルを読み込んで置換などの処理を行う例を紹介しましたが、ファイルの入出力や文字列の処理について、詳しくは下記のページなどを参照してください。

B.5.3 練習問題

ここまでに見たプログラムに自分なりにいろいろと機能を追加してみると、よい練習になると思います(とくに比較的プログラミングの経験が浅い方)。たとえば、次のような変更などをしてみるのはいかがでしょう。

  1. 上記のファイル関連のプログラムのすべてを、ファイル名をコードで指定するのではなく、コマンド行引数でしていたファイルに対して行うように変更せよ
  2. B.5.1 ファイルの内容を一度に読み込み」のプログラムを、1行ずつ読み込んで同じ処理をするように変更せよ
  3. B.5.2 ファイルを1行ずつ処理」のプログラムを、置換対象の文字列を別のファイルから読み込んで置換するように変更せよ
  4. 速度が重要な処理を行うようならば、「13.4 ベンチマーク」を参考にベンチマークをとって、上にあげた手法のうち、どれが効率がよいか確認せよ

B.6 ゴルーチン、チャネル、WaitGroup

Goでは「ゴルーチン」を使って並行実行を表現できます。並行に実行されている関数(ゴルーチン)間で情報をやり取りするのにチャネルを使えます。

最初のうちはなかなかピンと来ないかと思いますので、「10章 並行処理」と合わせて、この付録の例も試すと(少しは)わかりやすくなるかと思います。

B.6.1 チャネルを使った単純な例

まず最初に見るのは、単純なゴルーチンとチャネルの使用例です。

まず、関数main自体もゴルーチンとして起動される点に注意してください。この例では、mainの中で別のゴルーチンを起動するのに無名関数を使います。

図B-1のように、2つのゴルーチン間でchというチャネルを使ってデータをやり取りすることになります。この例の場合は無名関数からmainへの一方通行です。3つのメッセージが、❶❷❸の順番にチャネルに書き込まれ、この順番にmainで読み出されます。前のメッセージがチャネルから取り出されていない場合は、書き込み側が待たされます。一方、読み込み側では、チャネルにメッセージがなければそこで待ちが生じることになります。

関数<code class="tt">main</code>と無名関数の間のチャネルを介したデータのやり取り

図B-1 関数mainと無名関数の間のチャネルを介したデータのやり取り

ソースコードは次のとおりです(ch22b/08a-goroutine-simple1/main.go)。

// 無名関数を定義して、その無名関数の外の関数で定義されたchをそのまま使う
func main() {
    ch := make(chan string) // 文字列をやり取りするチャネルchを作る

    go func () {
        s1 := "メッセージ1"
        fmt.Println("chへ書き込み:", s1) // chへ書き込み: メッセージ1
        ch <- s1  // 外側の関数のチャネル変数chをそのまま使う

        s1 = "メッセージ2"
        fmt.Println("chへ書き込み:", s1) // chへ書き込み: メッセージ2
        ch <- s1

        s1 = "メッセージ3"
        fmt.Println("chへ書き込み:", s1) // chへ書き込み: メッセージ3
        ch <- s1
    }()  // () を書いて無名関数を実行する。()を書かないとコンパイル時のエラー

    s := <-ch
    fmt.Println("sをchから読み込み:", s) // sをchから読み込み: メッセージ1

    <-ch // スキップ。2つ目を飛ばすことになる

    s = <-ch
    fmt.Println("sをchから読み込み:", s) // sをchから読み込み: メッセージ3  //listend1
}

実行結果は次のようになります。

$ go run main.go 
chへ書き込み: メッセージ1
chへ書き込み: メッセージ2
sをchから読み込み: メッセージ1
chへ書き込み: メッセージ3
sをchから読み込み: メッセージ3

なお、ここでは説明しませんがch22b/08b-goroutine-simple2/main.goに、無名関数に引数を渡す例があります。

続いて、別の関数を定義して引数でチャネルを指定する例です(ch22b/08c-goroutine-simple3/main.go)。図B-2のように、別の関数subを定義して、引数でチャネルを渡してデータをやり取りしています。この例の場合もsubからmainへの一方通行です。

独立した関数(関数<code class="tt">main</code>と関数<code class="tt">sub</code>)の間のチャネルを介したデータのやり取り

図B-2 独立した関数(関数mainと関数sub)の間のチャネルを介したデータのやり取り

package main

import "fmt"

func main() {
        ch := make(chan string)
        go sub(ch) // subをchを引数としてゴルーチンとして起動する

        s := <-ch
        fmt.Println("sをchから読み込み:", s) // sをchから読み込み: メッセージ1
        <-ch // スキップ。2つ目を飛ばすことになる
        s = <-ch
        fmt.Println("sをchから読み込み:", s) // sをchから読み込み: メッセージ3
}

func sub(c3 chan<-string) { // チャネルに書き込みのみを行う指定
        s1 := "メッセージ1"
        fmt.Println("c3へ書き込み:", s1) // c3へ書き込み: メッセージ1
        c3 <- s1
        s1 = "メッセージ2"
        fmt.Println("c3へ書き込み:", s1) // c3へ書き込み: メッセージ2
        c3 <- s1
        s1 = "メッセージ3"
        fmt.Println("c3へ書き込み:", s1) // c3へ書き込み: メッセージ3
        c3 <- s1
}

B.6.2 ウェブサイトのチェック——WaitGroup

WaitGroupを使うことで、起動中のゴルーチンの数を数え、関係する全ゴルーチンの処理が終わるのを待つことができます。

例として自分が関与しているサイトが動作しているかをチェックするプログラムを見てみましょう(ch22b/09a-waitgroup/main.go)。なお、WaitGroupの詳細は「10.5.10 WaitGroupの利用」を、http.Clientの詳細は「11.4.1 クライアント」を参照してください*2

[*2] この例題は次のページを参考にしました——https://musha.com/scgo?ln=ax05
package main

import(
    "fmt"
    "sync"
    "net/http"
    "time"
)

func main() {
    var wg sync.WaitGroup // WaitGroupを使って終了してもよい時を判断
    websites := []string{  // チェックするサイトのURL。文字列のスライス
        "https://www.oreilly.co.jp/",
        "https://musha.com/",
        "https://marlin-arms.com/",
        "https://dictjuggler.net/",
        "http://localhost/",
    }

    client := &http.Client{
        Timeout: 200 * time.Millisecond, // タイムアウトの設定  ミリ秒単位 11章など参照
    }
    fmt.Printf("%T\n", client)

    for i, website := range websites { // 各サイトについて
        go checkWebsite(website, i, &wg, client) // ゴルーチンを生成
        wg.Add(1) // WaitGroupのカウンタを1増やす(処理が終了したら1減る)
    }

    wg.Wait() // WaitGroupのカウンタが0になるのを待つ
    // 0になれば全部確認が終わっているので、終了する
}

// checkWebsite 指定されたページを確認
func checkWebsite(url string, i int, wg *sync.WaitGroup, client *http.Client) {
    defer wg.Done() // 抜ける時にWaitGroupのカウンタを1減らす
    // 忘れないように冒頭にdeferを使って書いておく
    if res, err := client.Get(url); err != nil { // res: response
        fmt.Printf("%d %s  **ダウン** \n", i, url)
    } else {
        fmt.Printf("%d %s  code: %v\n", i, url, res.Status)
    }
}

実行結果は、たとえば次のようになります。

$ cd ch22b/09waitgroup 
$ go run main.go 
4 http://localhost/  **ダウン**
2 https://marlin-arms.com/  code: 200 OK
1 https://musha.com/  code: 200 OK
3 https://dictjuggler.net/  code: 200 OK
0 https://www.oreilly.co.jp/  code: 200 OK

単純にサーバを呼び出しては確認することを繰り返すと、間に待つだけの時間が生じてしまいますが、ゴルーチンを使えば待ち時間を減らせます。

なお、ch22b/09b-waitgroup-more/main.goにはサーバからのレスポンスに関してもう少し細かい情報を表示する例を置きました。

B.6.3 ウェブサイトのチェック——チャネル版

同じような処理をチャネルを経由してやってみます(ch22b/10a-server-check-goroutine/main.go)。サーバの状態を表す定数を定義しています(詳しくは「7.2.7 iotaと列挙型」参照)。最後の例なので少し複雑にしてみました。

package main

import(
    "fmt"
    "net/http"
    "time"
)

const TimeLimit time.Duration = 200 * time.Millisecond
// タイムリミットを設定
// const TimeLimit time.Duration = 80 * time.Millisecond

type ServerStatus int // サーバの状況を示す型ServerStatusを定義

const ( // ServerStatusが取りうる具体的な値(定数)の定義
    不明   ServerStatus = iota  // 「不明」に 整数の0が割り当てられる
    アップ    // 「アップ」に1
    問題発生  // 「問題発生」に2
)
// ↑変数に使える文字なら日本語も使える

type site struct {
    url string // URL文字列
    status ServerStatus  // 現状。「不明」「アップ」「問題発生」のいずれか
}

func main() {
    websites := []site {  // URLと現状。構造体のスライス
        {"https://www.oreilly.co.jp/", 不明},
        {"https://musha.com/", 不明},
        {"https://marlin-arms.com/", 不明},
        {"https://dictjuggler.net/", 不明},
        {"http://localhost/", 不明},
    }

    ch := make(chan site) // siteをやり取りするチャネルchを作る
    defer close(ch) // 最後にチャネルを閉じる
    client := &http.Client{ // 「11.4.1 クライアント」参照
        Timeout: time.Duration(TimeLimit), // タイムリミットの設定
    }
    for _, site := range websites { // 各サイトについて
        go checkWebsite(site, ch, client) // ゴルーチンを生成
    }

    for i:=0; i<len(websites); i++ { // サイトの数だけループ
        siteResponded := <-ch // どのサイトから返事が来るか、順番はわからない
        fmt.Printf("%s: %v\n", siteResponded.url, siteResponded.status)
        // チャネル経由で返事が来たサイトのURLと状況を書く
    }
}

// checkWebsite siteに指定されたページをチェックする
func checkWebsite(s site, ch chan<-site, client *http.Client) {
    if _, err := client.Get(s.url); err != nil { // urlから
        s.status = 問題発生
    } else {
        s.status = アップ
    }
    ch <- s
}

// ServerStatus 専用の関数(インタフェース)String。これを定義しておくことで、
// fmt.Printfに%vを指定した時に、数字ではなくここで指定した文字列で表示される
// 実行結果参照
func (ss ServerStatus) String() string {
    switch ss {
    case 不明:
        return "不明"
    case アップ:
        return "OK"
    case 問題発生:
        return "★問題発生★"
    default: // これがあれば、将来状態を追加しても何かが表示される
        return fmt.Sprintf("%d", int(ss))
    }
}

実行結果の例を示します。

$ cd ch22b/10a-server-check-goroutine 
$ go run main.go 
http://localhost/: ★問題発生★
https://marlin-arms.com/: OK
https://musha.com/: OK
https://dictjuggler.net/: OK
https://www.oreilly.co.jp/: OK

B.7 まとめ

比較的単純なGoのプログラムをいくつか紹介しました。皆さんがGoの世界に入るきっかけになれば幸いです。

この本が、次の半世紀のソフトウェア業界を背負って立つような開発者の皆さんのお役に立てれば幸いです。それでは、訳者が書いたコードが半世紀後もそのまま動作することを祈りつつ。ありがとうございました。