付録B 実例で学ぶGo言語入門
武舎 広幸
ほかの言語(特にCやC++)をある程度ご存じの方のための、サンプルプログラム集です。原著の最初のほうの章には、まとまったコードがあまり書かれていないので、以下に示す例題をざっと読んでおくと、Goの世界に馴染んで、本文の内容がわかりやすくなると思います。
訳者が原著を始めとする本などの情報を元に、これまでの経験から、できるだけわかりやすいと思う方法で書いてみました。タイムマシンで半年前に戻って、訳者自身がこれを読んだら「おかげで随分楽ができる」と思える内容にしたつもりです。
- JavaScript、Pythonなど型やポインタが表に出てこない言語が得意の方は、「2章 基本型と宣言」と6章の「6.1 ポインタ入門」と「6.2 ポインタを恐れる必要はない」に詳しい説明があります
- サンプルプログラムは本文の例題と同様、GitHubで公開しています(https://github.com/mushahiroyuki/lgo。書籍ではスペースに制約があるので省略したものも置いておきます)
B.1 予備知識
まず、サンプルコードを読むための予備知識です。
B.1.1 Go言語のコードの留意点
下のコードを読んだり試したりする上で、とくに次の点に留意してください。なお、「付録A Go言語のまとめ」には、より詳しい説明があります。さらに詳しくは、索引や目次などを頼りに本文を参照してください。
- 変数名を先に、型の指定は後に書く。たとえば「
var a int
」のようになる if
文やfor
文の条件などは(...)
で囲まないで書く- ループは
for
文だけ。while
やdo-while
を使うようなものもfor
で書けるようになっている(詳しくは「A.7 制御構造」や「4.3for
」参照) {
や}
の前後で改行してもいい位置とダメな位置がある(詳しくは1章のコラム「セミコロン挿入規則」参照)。たとえば関数の冒頭は次のように書く(「else if
」などについても同様)
func main() {
次のように書いてしまうとコンパイル時のエラーになる。
func main() {
- 定義だけして使わない変数があるとコンパイルできない(なお、定数は使わなくてもコンパイル時のエラーにはならない)
import
して利用しないパッケージ(ライブラリ)があるとコンパイルできない。前に「_
」を書くとエラーにはならなくなる(ただし、init
関数は実行される。「9.3.8init
関数」)
B.2 fmt
パッケージの動詞(verb)
これから見る例にも何度か登場するfmt.Printf
やfmt.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つのファイルが入っています。
main.go
——ソースコードファイルgo.mod
——この「モジュール」に関する情報を入れたファイル。以前のバージョンでは環境変数を使ったりしてソースや実行形式ファイルの配置を指定したりしていたが、最近のバージョンでは、このgo.mod
ファイルにモジュール(≒プロジェクト)に関する情報を記録することが標準になった(詳細は「9章 モジュールとパッケージ」参照)
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=ax04やhttps://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のうち、よく使われそうなもの、知っていると便利なものを使っています。
%s
、%q
——文字列。%q
だとエスケープされる(\n
\t
などを使って表記)%d
——整数。ベース(n進法)を変えることができて%b
、%o
、%x
、%X
でそれぞれ、2進、8進、16進(小文字のa-f)、16進(大文字のA-F)%f
、%F
あるいは%e
、%E
——%f
と%F
は浮動小数点数の一般的な小数点を使った表記(例:123.456)。%e
と%E
は指数表記(例:-1.234456e+78。%E
だとe
が大文字になる)%v
——デフォルトの形式で出力(システム側で「良きに計らってくれる」)%T
——型に関する情報を出力%%
——「%
」そのものを出力
%v
関連で、もう少し詳しい情報を出力してくれるものがあります。ここでは例を示しませんが、覚えておくとデバッグのときなどに便利かと思います。
%+v
——構造体のフィールド名も出力%#v
——Goのリテラル表現(2章など参照)で出力
なお、実行結果の13行目のように、verbに対応する変数などがない場合や、対応する変数などの型が指定とあっていない場合は、%!f
などのように「!
」が表示されますので、変数などとverbの対応を確認し修正してください。
このほか、パッケージfmt
についての詳細は次のページなどを参照してください。
- 公式ドキュメント——https://pkg.go.dev/fmt
- 日本語のページでよくまとまっていると思うページ——https://musha.com/scgo?ln=ax02
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
型の変数です。変数err
がnil
でないならばエラーが起こったことになります。その場合(および入力された値が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つの関数を下に示します。
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を抜けるだけになってしまう } } }
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 変数および定数の名前」)。ただし、次のように、英語(アルファベット)で記述することを前提としていると思える要素もあるので注意が必要です。
- モジュール外に公開する変数は大文字で始める(「9.3.1 インポートとエクスポート」)
- インタフェースの名前は通常「er」で終わる(「7.5 インタフェースとは」)
- 値を伴うコンテキストを生成する関数名は
contextWith
で始め、コンテキストから値を返す関数はFromContext
で終わる名前にする(「12.5 コンテキストによる値の伝搬」)
上の2つの関数を日本語の識別子を使って書き換えたものを下にリストします(全体はch22b/04guessnum-jpn/main.go
)。
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を抜けるだけになる } } }
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。ファイルに式を書いておいて一度に実行したり、コマンドの出力を入力として計算したりもできます。
bc
やsh
がありませんので、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桁ごとの区切りの「,
」が入っていたりするとエラーになってしまい、ウェブページから数値をコピーして計算したりするには(特に日本人にとっては)ちょっと不便な面もあります。そこで次のように改良(?)したコマンドを作ることにしましょう。
- 起動と同時に計算できるよう、コマンドの引数に計算したい式を指定できる(引数に「
*
」などの文字が使ってあるとシェルが特別な処理をしてしまうので、その場合は引用符で囲む必要がある) - 全角の数字や「
,
」(半角のカンマ)や「,
」(全角のカンマ)で区切られた数字も指定できる - 掛け算を
x
、X
、x
、X
、*
でも指定できる(最後の3つは全角) - 割り算を
÷
でも指定できる - 全角の「
.
」および「。
」も小数点として使える(全角モードでピリオドを打とうとすると「。
」が入力されてしまうので)
このプロジェクトはch22b/05a-calc-bc
の下にあり、ソースコードは2つのファイル(main.go
とcalculate.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 248832345,123÷12
次の計算をします: 345123/12 28760.25000000000000000000 ... いろいろ計算してみてください 改行のみで終了
コードに細かくコメントを書いてありますので詳細は省略しますが、次のような点に留意してください。
- ソースコードを
main.go
とcalculate.go
の2つに分けていますが、冒頭のpackage main
を共通にしておくだけで大丈夫です main.go
の冒頭のimport
で必要なライブラリ(モジュール)を読み込んで利用します。最後のgolang.org/x/text/width
は、全角・半角の処理を含む文字の「幅」関連の関数が含まれています。これを含むgolang.org/x/text
には主にUnicode関連のテキストを処理するためのさまざまなライブラリが含まれています- 正規表現を利用する際には、「コンパイル」してから利用します。
main.go
の関数replaceChars
では呼び出されるたびにコンパイルをしていますが、最初に一度だけコンパイルしたほうが効率的です(もっとも、対話的に利用する限り、このバージョンでも速度的な問題はないでしょう。なお、後述の「B.5.2 ファイルを1行ずつ処理」に一度だけコンパイルする例があります) - 実際の計算処理は
bc
コマンドに任せるので、外部コマンド(bc
)を呼び出します。この処理はcalculate.go
の関数calculate
で行っています
ファイル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つの例を紹介します。
- ファイルの内容全体を一度に読み込んで処理
- 1行ごとに読み込んで順番に処理
B.5.1 ファイルの内容を一度に読み込み
最近のパソコンは(昔に比べると)メモリ容量も大きいので、巨大なファイルでなければ、ファイル全体を読み込んで一度に処理してしまうと話が単純になります。
単純な例題ですが、ファイル中の表現の一斉置換をしてみましょう。次のような「辞書」のファイルがあるとします。
// 「//」で始まる行はコメント。英語と日本語はタブで区切る fungible 代替可能な, 代替性のある, 代替物 fungibility 代替可能性 // fungibility 代替できること pronunciation 発音
この辞書ファイルの形式が他の会社の辞書の形式と違っていたので、形式を揃えてほしいという要望が来ました。
- コメントは「
##
」で始まる行にしてほしい - 英語と日本語の間は「
|
」で区切ってほしい(タブは、設定によって幅が違って表示されるし、カラム数が多いと1行が長くなって見にくい)
たとえば、次のコードでこの変換ができます(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
には正規表現のマッチング部分を変数(スライス)に記憶して、以降の処理に利用して変換するバージョン(機能は同じ)があります。
ここまでファイルを読み込んで置換などの処理を行う例を紹介しましたが、ファイルの入出力や文字列の処理について、詳しくは下記のページなどを参照してください。
- ファイル入出力——パッケージ
os
(https://pkg.go.dev/os)やパッケージio
(https://pkg.go.dev/io)。なお日本語では次のページの説明がわかりやすいように感じました——https://musha.com/scgo?ln=ax03 - UTF-8の文字列処理——パッケージ
strings
(https://pkg.go.dev/strings) - 文字列をバイト列(byteのスライス)として処理(文字列に直してから変換するより効率的?)——パッケージ
bytes
(https://pkg.go.dev/bytes) - 正規表現——パッケージ
regexp
(https://pkg.go.dev/regexp)
B.5.3 練習問題
ここまでに見たプログラムに自分なりにいろいろと機能を追加してみると、よい練習になると思います(とくに比較的プログラミングの経験が浅い方)。たとえば、次のような変更などをしてみるのはいかがでしょう。
- 上記のファイル関連のプログラムのすべてを、ファイル名をコードで指定するのではなく、コマンド行引数でしていたファイルに対して行うように変更せよ
- 「B.5.1 ファイルの内容を一度に読み込み」のプログラムを、1行ずつ読み込んで同じ処理をするように変更せよ
- 「B.5.2 ファイルを1行ずつ処理」のプログラムを、置換対象の文字列を別のファイルから読み込んで置換するように変更せよ
- 速度が重要な処理を行うようならば、「13.4 ベンチマーク」を参考にベンチマークをとって、上にあげた手法のうち、どれが効率がよいか確認せよ
B.6 ゴルーチン、チャネル、WaitGroup
Goでは「ゴルーチン」を使って並行実行を表現できます。並行に実行されている関数(ゴルーチン)間で情報をやり取りするのにチャネルを使えます。
最初のうちはなかなかピンと来ないかと思いますので、「10章 並行処理」と合わせて、この付録の例も試すと(少しは)わかりやすくなるかと思います。
B.6.1 チャネルを使った単純な例
まず最初に見るのは、単純なゴルーチンとチャネルの使用例です。
まず、関数main
自体もゴルーチンとして起動される点に注意してください。この例では、main
の中で別のゴルーチンを起動するのに無名関数を使います。
図B-1のように、2つのゴルーチン間でch
というチャネルを使ってデータをやり取りすることになります。この例の場合は無名関数からmain
への一方通行です。3つのメッセージが、❶❷❸の順番にチャネルに書き込まれ、この順番に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
への一方通行です。
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 。
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の世界に入るきっかけになれば幸いです。
この本が、次の半世紀のソフトウェア業界を背負って立つような開発者の皆さんのお役に立てれば幸いです。それでは、訳者が書いたコードが半世紀後もそのまま動作することを祈りつつ。ありがとうございました。