付録B 実例で学ぶGo言語入門
武舎 広幸
ほかの言語(特にCやC++)をある程度ご存じの方のための、サンプルプログラム集です。原著の最初のほうの章には、まとまったコードがあまり書かれていないので、以下に示す例題をざっと読んでおくと、Goの世界に馴染んで、本文の内容がわかりやすくなると思います。
訳者が原著を始めとする本などの情報を元に、これまでの経験から、できるだけわかりやすいと思う方法で書いてみました。訳者自身がタイムマシンでGo言語を学ぶ前に戻ってこれを読んだら、「おかげで随分楽ができる」と思える内容にしたつもりです。
- 括弧内に記載された章や節のタイトルは、詳しい情報が記載されている場所を示しています
- JavaScript、Pythonなど型やポインタが表に出てこない言語が得意の方には、「2章 事前宣言された型」と6章の「6.1 ポインタ入門」と「6.2 ポインタを恐れる必要はない」に詳しい説明があるので一読をお勧めします。
- サンプルプログラムは本文の例題と同様、GitHubで公開しています(https://musha.com/go2。書籍では省略したものもあります)
B.1 予備知識
まず、サンプルコードを読むための予備知識です。
B.1.1 Go言語のコードの留意点
本稿のコードを読んだり試したりする上で、特に次の点に留意してください。なお、「付録A Go言語のまとめ」には、より詳しい説明があります。さらに詳しくは、索引や目次などを頼りに本文を参照してください。
- 変数名を先に、型の指定は後に書く。たとえば整数型の変数
aを宣言するには「var a int」とする if文やfor文の条件などは(...)で囲まないで書く- ループは
for文だけ。whileやdo-whileを使うようなものもforで書けるようになっている(「A.7 制御構造」「4.3for」) {や}の前後で改行してもいい位置とダメな位置がある(1章のコラム「セミコロン挿入規則」)。たとえば関数の冒頭は次のように書く(「else if」などについても同様)func main() {次のように書いてしまうとコンパイル時のエラーになる。
func main() {- 定義だけして使わない変数があるとコンパイルできない(なお、定数は使わなくてもコンパイル時のエラーにはならない)
importして利用しないパッケージ(多くの言語の「ライブラリ」に相当するもの)があるとコンパイルできない。前に「_」を書くとエラーにはならなくなる(ただし、init関数は実行される。「10.3.10init関数」)
B.2 fmtパッケージのverb
これから見る例にも何度か登場するfmt.Printfやfmt.Scanfなどを含むパッケージfmtは、入出力やフォーマット(書式)に関する機能を提供してくれます(C言語のprintfなどやscanfなどと似ています)。
下のコードで使われているfmt.Printfなど、パッケージfmtの関数のうち、最後にfが付いているものには、第1引数に文字列を指定して出力や入力のフォーマット(format)を指定します。このときに%dや%sなどのverbが使われます*1。
[*1] 「%dや%sはverb(動詞)か?」という疑問をもつかもしれませんが、たとえば%dは「整数を表示する」という「動作」を表すのでverbと呼んでいるようです。
fmt.Printf("第%d問の答えは%sです\n", i, answer) // 標準出力(通常は画面)に出力
原著にverbに関する記載があまりないので、最初の例題として、verbのうち、よく使われるものや、便利と思われるものを紹介します。例題ディレクトリ(フォルダ)の下のch22b/00fmt-verbにコードを置きますので、次の手順を参考に実行してみてください。
B.2.1 例題ディレクトリの構成
ディレクトリexample/ch22b/00fmt-verbには次の2つのファイルが入っています。
main.go——ソースコードファイルgo.mod——このモジュール(≒プロジェクト)に関する情報を入れたファイル。Goの初期のバージョンでは環境変数を使ったりしてソースや実行形式ファイルの配置等を指定していたが、現在は、このgo.modファイルにモジュールに関する情報を記録する(「10章 モジュールとパッケージ」)
B.2.2 プログラムの実行
実行するには次のコマンドを入力してください(「11.1 go run——短いプログラムの実行」)。
$cd example/ch22b/00fmt-verb# 例題ディレクトリの00fmt-verbに移動 $go run main.go
あるいは次のようにしても大丈夫です。go buildでコンパイル後fmt-verbという実行形式のファイルができるので、それを実行します。コンパイラは同じディレクトリにあるファイルgo.modを参照してfmt-verbというファイル名を決めます(「1.2.2 go build」)。
$cd example/ch22b/00fmt-verb$go build$./fmt-verb
なお、最初の方法(go run main.goを実行)では、一時ファイルに実行形式ができてそれが実行され、終了すると削除されます。例をちょっと試すといった場合には、最初の方法が便利です。
また、go build -o xxxのようにしてもxxxという実行形式のファイルができますので、./xxxでコマンドを実行できます(go.modに従った実行形式ファイル名の指定より優先されます)。
go buildコマンドを使うと、(とても)簡単にクロスコンパイル(ほかの環境で動作する実行ファイルの作成)できます。詳しくは「11.11 ほかのプラットフォーム用のGoバイナリのビルド」を参照してください。
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——浮動小数点数の一般的な小数点を使った表記(例:123.456)%e、%E——浮動小数点数の指数表記(例:-1.234456e+78。%Eだとeが大文字になる)%v——デフォルトの形式で出力(システム側で「良きに計らって」くれる。verbのvと思われる)%T——型に関する情報を出力%%——「%」そのものを出力
%v関連で、もう少し詳しい情報を出力してくれるものがあります。ここでは例を示しませんが、覚えておくとデバッグのときなどに便利かと思います。
%+v——構造体のフィールド名も出力%#v——Goのリテラル表現で出力(「2章 事前宣言された型」など)
なお、実行結果の13行目のように、verbに対応する変数などがない場合や、対応する変数などの型が指定と合っていない場合は、%!fなどのように「!」が表示されますので、変数などとverbの対応を確認し修正してください。
fmt.Printfでは標準出力(通常画面)に文字列を出力しますが、fmt.Sprintfを用いると同じverbを使うことで細かなフォーマットを指定して、文字列を作成できます。
s := fmt.Sprintf("第%d問の答えは%sです\n", i, answer)
// 文字列型の変数sに指定のフォーマットの文字列が代入される
このほか、パッケージfmtについての詳細は公式ドキュメント(https://pkg.go.dev/fmt)などを参照してください。
B.3 基本構文と標準入出力
次はGo言語の基本的な構文を紹介します。1以上10以下の数字を当てるゲームです。ch22b/01guessnum1に移動してから、「go run main.go」を実行して、試しながら説明を読むことをお勧めします。
では実際のコードを見ていきましょう。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 変数 型」の順。変数には型の「ゼロ値」が入る
//(この場合は空文字列""になる。整数ならゼロ値は0(「2.1.1 ゼロ値」)
fmt.Scanln(&inp) // 文字列として読み込み。&はポインタを表す
// inpの値を書き換えてもらうためにポインタを渡す(「6章 ポインタ」)
// Scanlnは読み込んだ単語数(スペースあるいは改行区切り)とエラーを戻すが
// 今回はどちらも無視するので、結果を変数に代入していない
// 下で整数に変換できなければエラーになるのでこれで大丈夫
num, err := strconv.Atoi(inp) // 整数に変換(Ascii TO Integer)
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言語に例外処理*2はありません。関数から複数の値を返すことができ、返す複数の値の最後にerror型を指定することで例外(やエラー)を処理するのが一般的です。
[*2] 想定していないことが起こった場合や、通常の処理手順とは異なる処理をしなければならないときに特別な処理をするための仕組み。比較的最近開発されたプログラミング言語では、例外処理(エラー処理)用に特別な構文が用意されていることが多いが、Goでは例外処理用に特別な構文は用意されていない(「9章 エラー処理」)。
上の例では、文字列(「半角」の数字列)を整数に変換する関数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" // Go 1.20から不要になった(乱数のseed(種)を生成するためのものだった)
)
// main
func main() {
answer := getTheNumber(); // 関数呼び出し。答えの数字をもらう
for count:=1; ; count++ { //「条件」を指定しないと無限ループになる
printPrompt(count) // 説明のメッセージを表示
if num, err := readUserAnswer();
err != nil || num < 1 || 10 < num {
// 条件の前で変数を宣言し、値の代入もできる。「err != nil」以降が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()) // Go 1.20から不要になった(乱数のseedの設定用)
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 変数および定数の名前」)。ただし、次のように、英語(アルファベット)で記述することを前提としていると思える要素もあるので注意が必要です。
- モジュール外に公開する変数は大文字で始める(「10.3.1 エクスポートする識別子の指定」)
- インタフェースの名前は通常「er」で終わる(「7.5 インタフェース」)
- 値を伴うコンテキストを生成する関数名は
contextWithで始め、コンテキストから値を返す関数はFromContextで終わる名前にする(「14.2 コンテキストによる値の伝播」)
上の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で簡単に計算ができます*3。ファイルに式を書いておいて一度に実行したり、コマンドの出力を入力として計算したりもできます。
[*3] Windowsには標準ではbcやshがありませんので、Goの範囲内で式の評価を行ってしまうバージョンを作っておきました。ch22b/05b-calc-evalの下を参照してください。なお、Windowsでもbcを動かすことができます。次のページなどを参照してください——https://musha.com/scgo?ln=ax01
たとえば次のような感じで使えます(引数-l——小文字のL——を付けないと結果が整数になります)。
$bc -l3400*23400×2を計算 6800 答えは 6800(3330+95-120)*1.1(3330+95-120)×1.1 3635.5 上の答え12^312の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^512の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 { // 引数があったら
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)に変更して実行すると次のようなエラーメッセージが表示されることになります。
2025/05/22 14:44:19 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.gofungible|代替可能な 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個でないとき
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) - UTF-8の文字列処理——パッケージ
strings(https://pkg.go.dev/strings) - 文字列をバイト列(byteのスライス)として処理(文字列に直してから変換するより効率的?)——パッケージ
bytes(https://pkg.go.dev/bytes) - 正規表現——パッケージ
regexp(https://pkg.go.dev/regexp)
B.6 ゴルーチン、チャネル、WaitGroup
Goでは「ゴルーチン」を使って並行実行を表現できます。並行に実行されている関数(ゴルーチン)間で情報をやり取りするのにチャネルを使えます。
最初のうちはなかなかピンと来ないかと思いますので、「12章 並行処理」と合わせて、この付録の例も試すと(少しは)わかりやすくなるかと思います。
B.6.1 チャネルを使った単純な例
最初に見るのは、単純なゴルーチンとチャネルの使用例です。
まず、関数main自体もゴルーチンとして起動される点に注意してください。この例では、mainの中で別のゴルーチンを起動するのに無名関数を使います。
図B-1のように、2つのゴルーチン間でchというチャネルを使ってデータをやり取りすることになります。この例の場合は無名関数からmainへの一方通行です。3つのメッセージが、❶❷❸の順番にチャネルに書き込まれ、この順番にmainで読み出されます。前のメッセージがチャネルから取り出されていない場合は、書き込み側が待たされます。一方、読み込み側では、チャネルにメッセージがなければそこで待ちが生じることになります。
図B-1 関数mainと無名関数の間のチャネルを介したデータのやり取り
ソースコードは次のとおりです(ch22b/08a-goroutine-simple1/main.go)。
// 無名関数を定義して、その無名関数の外の関数で定義されたchをそのまま使う
func main() {
ch := make(chan string) // 文字列をやり取りするチャネルchを作る
go func () { // 無名関数を定義。goが頭につくとゴルーチンとして実行
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
}
実行結果は次のようになります。
$ 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への一方通行です。
図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の詳細は「12.5.9 WaitGroupの利用」を、http.Clientの詳細は「13.4.1 HTTPクライアント」を参照してください*4。
[*4] この例題は次のページを参考にしました——https://medium.com/wesionary-team/practical-example-of-concurrency-on-golang-fc4609ea8ed1
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{ // 「13.4 net/http」参照
Timeout: 200 * time.Millisecond, // タイムアウトの設定 ミリ秒単位 「11.2.1 時間」参照
}
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.go4 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 = 1000 * 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.gohttp://localhost/: ★問題発生★ https://marlin-arms.com/: OK https://musha.com/: OK https://dictjuggler.net/: OK https://www.oreilly.co.jp/: OK
B.7 まとめ
比較的単純なGoのプログラムをいくつか紹介しました。皆さんがGoの世界に入るきっかけになれば幸いです。
この本が、次の半世紀のソフトウェア業界を背負って立つような開発者の皆さんのお役に立てれば幸いです。それでは、訳者が書いたコードが半世紀後もそのまま動作することを祈りつつ。ありがとうございました。
マーリンアームズ