はじめに
前回は環境構築からHello Worldを出力するまでの基本的なアプリを作成しました。
前回の記事はこちら。
今回は基本的なことをおさらいしながら、ほかのモジュールから呼び出せる関数を持つ小さなモジュールを作成します。
モジュールの作成
まずはモジュールを作成してみましょう。
モジュールとは1つ以上の関連するパッケージを集めて、便利な関数のセットにしたものです。コードをまとめたのがパッケージ。パッケージをまとめたのがモジュールとなるのです。
まずはgreetings
というフォルダを作成して、go mod init example.com/greetings
をコマンドプロンプトに入力してgreetings
パッケージを作成しましょう。この例ではexample.com
としていますが、モジュールを公開する場合はダウンロードできるパスにしてください。
まずはgreetings.go
ファイルを作成して、以下のコードを追加しましょう。
package greetings
import "fmt"
// 指定された名前の挨拶を返す
func Hello(name string) string {
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message
}
コードの解説
このコードでは以下のことをしています。
greetings
(挨拶)パッケージを宣言fmt
パッケージをインポート- 挨拶を返す関数を実装
関数
先ほどのコードではfunc Hello(name string) string {}
の様に関数を宣言していました。Goでは関数をfunc 関数名(引数名 引数の型) 戻り値の型 {}
の様に宣言していきます。
Goでは名前が大文字から始まる関数は、外部から呼び出すことのできる関数となり、エクスポート名と呼ばれます。
変数
関数の中ではmessage := fmt.Sprintf("Hi, %v. Welcome!", name)
というコードを記述しました。
これは変数の宣言と初期化を同時に行っています。:=
演算子が変数の宣言と初期化を表す演算子となり、右側のfmt.Sprintf
で得た値を左のmessage
変数に格納して初期化します。
宣言と初期化を分けたい場合は、ほかの言語の様に以下のコードで表すこともできます。
var message string
message = fmt.Sprintf("Hi, %v. Welcome!", name)
fmt.Sprintf関数
fmt.Sprintf
を使用すると引数で受け取ったname
を含めて文字列を作成することがあります。今回は"Hi, %v. Welcome!"
の%v
にname
の値が入った文字列を返します。
return
他の言語と同様にreturn
を記述すると関数はそこで終了し、パラメータを付けることができます。
別のモジュールからコードを使用する
先ほどは挨拶モジュールを作成しました。ここからは先ほど作った挨拶モジュールのHello
関数を呼び出すコードを書いていき、アプリケーションを実行できる状態にします。
Helloモジュールの作成
まずはhello
フォルダを作成しましょう。以下のようにhello
フォルダとgreetings
フォルダが同じ階層になるように作成してください。
<home>/
|-- greetings/
|-- hello/
これから書くコードの依存関係を追跡するために、依存性トラッキングを有効にします。go mod init
コマンドを実行して有効にしましょう。
go mod init example.com/hello
コマンドをコマンドプロンプトに入力して追跡できるようにしましょう。前回と同じように公開する場合は、example.com
を変更してください。
前回の環境構築のテストで、モジュールをすでに作成している場合は中身を一度削除し、初期化しなおしましょう。
無事初期化ができたら早速hello.go
を作成して、以下のコードを追加しましょう。
package main
import (
"fmt"
"example.com/greetings"
)
func main(){
// 挨拶のメッセージをもらって、出力をする
message := greetings.Hello("Gladys")
fmt.Println(message)
}
import
ディレクティブの書き方が変わりましたね。複数のモジュールをインポートする場合は括弧で囲むことでもインポートすることができます。もちろんすべてimport "xxx"
の様に書くこともできますが、Googleは複数の場合は括弧で囲むことを推奨しています。
公開予定のモジュールを未公開で使用する
example.com/greetings
はGoツールがexample.comにアクセスしてモジュールをダウンロードします。ですが、チュートリアルで使用しているだけなので実際にモジュールをダウンロードすることはできません。そこでexample.com/greetings
を指定されたら、ローカルのgreetings
フォルダを使うように設定する必要があります。
go.mod
ファイルに設定が書き込まれているので、go mod edit
コマンドを使用して編集します。go mod edit -replace example.com/greetings=../greetings
コマンドを使用して設定を変更してください。
上記コマンドを実行すると、以下の様に変換する設定が追加されていることを確認できます。
<!-- 省略 -->
replace example.com/greetings => ../greetings
設定を書き込んだらgo mod tidy
を実行してモジュールの依存関係を整理してみましょう。その後もう一度go.mod
ファイルを開いて中身を確認してみてください。
<!-- 省略 -->
replace example.com/greetings => ../greetings
require example.com/greetings v0.0.0-00010101000000-000000000000
require
ディレクティブが追加されたことを確認できると思います。このrequire
ディレクティブはgreetings
ディレクトリのローカルコードを見つけた際に追加されます。モジュールはまだ公開されておらず、バージョンが付与されていないので、ここで疑似的なバージョンを用意して管理します。
実行確認
go run .
コマンドを実行して挨拶がコマンドプロンプトに出力されるかを確認しましょう。Hi, Gladys. Welcome!
が出力されたら成功です。
エラーハンドリング
エラーを正しく処理する機能は必須の機能です。次はgreetings
モジュールからエラーを返して呼び出し側で正しく処理するようなコードを追加してみましょう。
先ほどは「名前を教えてもらい挨拶を返す関数」を作成しました。しかし、名前が分からなかったらどうでしょう。正しく挨拶を返していると言えるのでしょうか。早速名前が空の場合はエラーを返すようにしてみましょう。
package greetings
import (
"errors"
"fmt"
)
// Hello関数は指定された人物への挨拶を返す。
func Hello(name string) (string, error) {
// 名前が与えられていない場合はメッセージとともにエラーを返す。
if name == "" {
return "", errors.New("empty name")
}
// 名前を受け取った場合は、名前を埋め込んだ挨拶メッセージを返す。
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message, nil
}
このコードではこのようなことを行っています。
- 標準ライブラリである
errors
パッケージをインポート。 - 戻り値をメッセージとエラー情報の2つ返すように変更。
- if文を追加して無効なリクエスト(名前が空)かどうかをチェックして無効な場合はエラーを返す。
- 成功した場合のエラー情報はエラーなしを表す
nil
を返す。
複数の戻り値
Goでは複数の戻り値を持つことができます。少し変わった特徴ですが、エラーを返すのによく使われています。func 関数名(変数名 変数型) (戻り値1の型, 戻り値2の型){}
の様に関数の宣言時に戻り値を括弧で囲んで複数宣言することで使用可能です。
エラーの返し方
エラーを返すにはimport "errors"
を追加してerrors
パッケージを利用します。errors.New(string)
を使用することでエラー情報を作成することができ、戻り値に含めることで呼び出し元にエラーの発生を通知することが可能です。nil
を返すことで成功したことを表すことができます。
nilとは
筆者はC#を主に使用していたのでnull
の概念を把握していましたが、nil
というのは初耳でした。両方とも「値を持たない」ということを表すようです。Goではnull
を使用できませんが、間違った認識だと想定と動作が異なってしまう恐れがあるのでまとめました。
項目 | 内容 | 自身の理解 |
---|---|---|
nil | 特定の型の変数が値を持たないことを表す。型の情報を持つ。 | *int32 のnil と*int64 のnil を比較しても一致しない。例:error型の変数 err には中身が入っていない |
null | オブジェクトが存在しないことを表す。型の情報を持たない。 | *int32のnullと*int64のnullを比較しても一致しない。 例:error型の変数 err は参照されておらずデータが存在しない |
エラーの受け取り方
Hello関数の戻り値が二つに変更されたので、呼び出し元も変更する必要があります。以下のコードに変更してみましょう。
package main
import (
"fmt"
"log"
"example.com/greetings"
)
func main() {
// ログエントリーの接頭辞と、時間・ソースファイル・行番号の表示を無効にするフラグなど、定義済みロガーのプロパティを設定します。
log.SetPrefix("greetings: ")
log.SetFlags(0)
// あいさつを要求.
message, err := greetings.Hello("")
// エラーが返ってきたらエラーログを出力してプログラムを終了する。
if err != nil {
log.Fatal(err)
}
// エラーがなければ受け取ったメッセージを返す。
fmt.Println(message)
}
このコードはこんなことをしています。
log
パッケージのインポート- タイムスタンプやソースファイル情報なしで、先頭にコマンド名(
greetings:
)を付けてログを出力するように設定 Hello
関数を実行して返ってきた二つの戻り値をmessage
とerr
に代入Hello
関数の名前を空文字に変更して、エラー処理を確認できるように変更err
がnil
ではない場合エラーとする- 標準ライブラリである
log
パッケージのFatal
関数を使用してエラーメッセージを出力し終了させる
logパッケージについて
log
パッケージは標準ライブラリの一つでログ出力をサポートします。今回はlog.Fatal(err)
を使用し、return
がなくても終了できるのはFatal
関数がos.Exit(1)
を呼んでいるからです。
また、Go 1.21からslog
パッケージが標準ライブラリとして追加されました。このライブラリは構造化ロギングパッケージでlog/slog
をインポートすることで使用することができます。log
パッケージでできなかった様々な機能が追加されているので、頭の片隅に置いておきましょう。
実行確認
いつも通りコマンドプロンプトでhello
ディレクトリに移動し、go run .
を実行しましょう。以下の内容が出力されたら成功です。
greetings: empty name
exit status 1
ランダムな挨拶を返す
現在は一種類しかありますが、挨拶にはいろんな仕方があります。あらかじめいくつかの挨拶メッセージを決めて、その中の一つを返すように変更してみましょう。
スライスを使用する
今回はGoのスライスを使用します。スライスは配列なようなもので、項目を追加・削除することによってサイズが動的に変化します。Goではよく使われる便利な型ですので、おぼえておきましょう。
まずは3つの挨拶メッセージを含む小さなスライスを追加して、ランダムにメッセージの一つを返すように変更します。
package greetings
import (
"errors"
"fmt"
"math/rand"
)
// Hello関数は指定された人物への挨拶を返す。
func Hello(name string) (string, error) {
// 名前が与えられていない場合はメッセージとともにエラーを返す。
if name == "" {
return "", errors.New("empty name")
}
// ランダムなフォーマットでメッセージを作成する。
message := fmt.Sprintf(randomFormat(), name)
return message, nil
}
// randomFormat は、挨拶メッセージのセットを返します。
// 返されるメッセージはランダムに選択されます。
func randomFormat() string {
// メッセージ候補を集めたスライスを作成
formats := []string{
"Hi, %v. Welcome!",
"Great to see you, %v!",
"Hail, %v! Well met!",
}
// formatsのスライスにランダムなインデックスを指定して、
// ランダムに選択されたメッセージ・フォーマットを返す。
return formats[rand.Intn(len(formats))]
}
このコードでは以下のことをしています。
- 挨拶メッセージの書式をランダムに選択して返す
randomFormat
関数を追加。
(randomFormat
関数は小文字から始まっているのでエクスポートされず、パッケージ内でしか利用できない) randomFormat
で3つのメッセージフォーマットをもつスライスを宣言。
宣言するには[]string
の様に大括弧を型の前に付けることで指定した型のスライスを作成することができます。math/rand
パッケージを使って、スライスからアイテムを選択するための乱数を生成する。Hello
関数ではrandomFormat
関数を呼び出して返すメッセージの書式を取得し、その書式と名前の値を組み合わせてメッセージを作成する- メッセージもしくはエラーを返す
エラーを出さないように変更
前回の内容ではhello
パッケージから呼び出す際に、名前を入力せずに実行していました。今回はエラーを出したくないため、名前を入力して確認しましょう。
package main
import (
"fmt"
"log"
"example.com/greetings"
)
func main() {
// ログエントリーの接頭辞と、時間・ソースファイル・行番号の表示を無効にするフラグなど、定義済みロガーのプロパティを設定します。
log.SetPrefix("greetings: ")
log.SetFlags(0)
// あいさつを要求.
message, err := greetings.Hello("Gladys")
// エラーが返ってきたらエラーログを出力してプログラムを終了する。
if err != nil {
log.Fatal(err)
}
// エラーがなければ受け取ったメッセージを返す。
fmt.Println(message)
}
実行確認
いつも通りコマンドプロンプトでhello
ディレクトリに移動し、go run .
を実行しましょう。数回実行して、挨拶の内容が変化することを確認してください。
$ go run .
Great to see you, Gladys!
$ go run .
Hi, Gladys. Welcome!
$ go run .
Hail, Gladys! Well met!
複数の人に挨拶をする
現在は一回につき一人分のメッセージしか取得できません。複数人のメッセージを取得するには何回もHello
関数を実行しなければなりません。ですので一回のリクエストで複数人の挨拶を取得できるように変更しましょう。
しかし、モジュールを公開している場合はほかの人がすでに使用しているかもしれないということを考慮しなければなりません。既存の関数のシグネチャを変更すると、破壊的な変更となってしまいます。
今回は後方互換性を保つために新しい関数を追加して、複数人のサポートをします。
複数人に対応した関数の追加
greetings.go
に以下のコードを追加します。
// ...
func Hello(name string) (string, error) {
// ...
}
// Hellosは、名前を付けられた人それぞれに挨拶メッセージを関連付けたマップを返す。
func Hellos(names []string) (map[string]string, error) {
// 名前とメッセージを関連付けるためのマップを作成
messages := make(map[string]string)
// 受信した名前のスライスをループし、Hello関数を呼び出してそれぞれの名前のメッセージを取得する。
for _, name := range names {
message, err := Hello(name)
if err != nil {
return nil, err
}
// マップの中で、取得したメッセージを名前に関連付ける。
messages[name] = message
}
return messages, nil
}
// ...
このコードではこんなことを行っています。
- パラメーターが単一の名前ではなく、名前のスライスである
Hellos
関数を追加する。戻り値の型を文字列からマップに変更して、挨拶メッセージにマップされた名前を返せるようにする。 - 新しいHellos関数が既存のHello関数を呼び出すようにする。
(同じ処理を書く必要がなくなり、両方の関数を残すことができます) map
を使用して受信した名前をキー、生成されたメッセージを値として関連付けする。
(make(map[キーの型]値の型)
でマップを初期化することができます)- 関数が受け取った名前をループして、それぞれ適切な名前を持っていることをチェックし、メッセージを関連付けます。この
for
ループではrange
を使用しています
forループ
Goではfor
文を使用してforeach
や無限ループを表すことが可能です。for
とrange
を使用してfor インデックスの変数名, データの変数名 := range 配列もしくはスライスの変数
という表し方でforeach
を表します。
インデックスは今回使用しないため_
(空白識別子)を使用して破棄しています。インデックスのみ使用したい場合は、データの変数名省略することもできます。
複数の名前とメッセージフォーマットを取得する
作成したHellos
関数を使用して複数の名前を渡し、複数の名前とメッセージフォーマットを取得できるように変更しましょう。
package main
import (
"fmt"
"log"
"example.com/greetings"
)
func main() {
// ログエントリーの接頭辞と、時間・ソースファイル・行番号の表示を無効にするフラグなど、定義済みロガーのプロパティを設定します。
log.SetPrefix("greetings: ")
log.SetFlags(0)
// 名前のスライスを作成
names := []string{"Gladys", "Samantha", "Darria"}
// 複数の名前であいさつを要求.
messages, err := greetings.Hellos(names)
// エラーが返ってきたらエラーログを出力してプログラムを終了する。
if err != nil {
log.Fatal(err)
}
// エラーがなければ受け取ったメッセージを返す。
fmt.Println(messages)
}
3つの名前を保持するスライス型のnames
を用意して、Hellos
関数の引数として渡すように変更しました。
実行確認
いつも通りコマンドプロンプトでhello
ディレクトリに移動し、go run .
を実行しましょう。名前とメッセージを関連付けたマップの文字列表現が表示されるはずです。
$ go run .
map[Darria:Great to see you, Darria! Gladys:Hail, Gladys! Well met! Samantha:Hail, Samantha! Well met!]
単体テスト
これまでで挨拶のモジュールにコードを一通り追加しました。Goには単体テストの機能が組み込まれており、go test
コマンドを使用することでテストを書いて実行することができます。バグがないか確認するためにもテストを作成しましょう。
テストコードの追加
今回はHello
関数のテストを追加します。greetings
でディレクトリに移動して、以下の内容のgreetings_test.go
ファイルを作成しましょう。Goではファイル名の最後に_test.go
を付けることで、通常のパッケージのビルドからは除外され、go test
コマンドを使用したテストでのビルドには含めることができます。
package greetings
import (
"testing"
"regexp"
)
// TestHelloNameは、名前を指定してgreetings.Helloを呼び出し、
// 有効な戻り値があるかどうかをチェックする。
func TestHelloName(t *testing.T) {
name := "Gladys"
want := regexp.MustCompile(`\b`+name+`\b`)
msg, err := Hello("Gladys")
if !want.MatchString(msg) || err != nil {
t.Fatalf(`Hello("Gladys") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
// TestHelloEmptyは空の文字列でgreetings.Helloを呼び出し、
// エラーをチェックする。
func TestHelloEmpty(t *testing.T) {
msg, err := Hello("")
if msg != "" || err == nil {
t.Fatalf(`Hello("") = %q, %v, want "", error`, msg, err)
}
}
このコードではこんなことをしています。
- テスト対象と同じパッケージにテスト関数を実装する。
(_test
を付けたパッケージに置くこともできるが、その場合はテスト対象のパッケージを明示的にインポートする必要があり、エクスポートされた識別子しか使用できないためブラックボックステストとなる) greetings.Hello
関数をテストするための2つの関数を作成する。
テスト関数の名前にはTestXxxという形式があり、Xxxには大文字から始まる特定のテストに関する名前を付ける必要があります。また、テスト関数はtesting
パッケージのtesting.T
型へのポインタをパラメーターとして受け取ります。このパラメーターのメソッドを使用して、テストからのレポートやロギングを行います- テストの実装
TestHelloName
はHello
関数を呼び出して、関数が有効な応答メッセージを返せるような名前の値を渡します。呼び出しがエラーまたは予期しない応答メッセージを返した場合、t
パラメーターのFatalIf
メソッドを使用してメッセージを出力して、実行を終了させます。TestHelloEmpty
はHello
関数を空の文字列で呼び出します。このテストはエラー処理が機能するかを確認するためのテストです。空ではない文字列を返すか、エラーがなければt
パラメーターのFatalIf
メソッドを使用してメッセージを出力して、実行を終了させます。
テストの実行
テストコードが完成したのでテストを実行してみましょう。
成功した場合
greetings
ディレクトリに移動し、go test
コマンドを実行してテストを実行します。go test
コマンドはテストファイル内のテスト関数を実行します。v
フラグを追加すると、すべてのテストと結果を一覧表示する出力を得ることができます。
$ go test -v
=== RUN TestHelloName
--- PASS: TestHelloName (0.00s)
=== RUN TestHelloEmpty
--- PASS: TestHelloEmpty (0.00s)
PASS
ok example.com/greetings 0.199s
失敗した場合
greetings.Hello
関数を変更してわざと失敗するように変更してみましょう。fmt.Sprintf
に名前を含めないように変更して実行します。
func Hello(name string) (string, error) {
// ...
// わざと失敗するように変更
message := fmt.Sprintf(randomFormat())
// ...
}
go test
--- FAIL: TestHelloName (0.00s)
greetings_test.go:15: Hello("Gladys") = "Hail, %!v(MISSING)! Well met!", <nil>, want match for `\bGladys\b`, nil
FAIL
exit status 1
FAIL example.com/greetings 0.191s
上記のようなエラーメッセージが出たら失敗扱いです。
実行ファイルの生成
さて、今までgo run
コマンドで実行確認をしてきましたが、実行ファイルを生成しているわけではないのでリリースができません。そこで、実行ファイルを生成するgo build
コマンドやgo install
コマンドについて学んでいきましょう。
実行ファイルのコンパイル
実際にコマンドを実行して実行ファイルを生成してみましょう。hello
ディレクトリに移動して、go build
コマンドを実行しましょう。
hello
ディレクトリに実行ファイルであるhello.exe
が生成されていると思います。コマンドプロンプトから実行して、正常に動作するかを確認してみましょう。
$ .\hello.exe
map[Darria:Hail, Darria! Well met! Gladys:Great to see you, Gladys! Samantha:Hi, Samantha. Welcome!]
無事に実行ファイルを生成することができました。しかし、現在実行するにはコマンドプロンプトで対象のディレクトリに移動する必要があります。
続いてパスを指定しないで実行できるように、実行ファイルをインストールしてみましょう。
Goのインストールパスの確認
go list
コマンドを実行することで、インストールパスを検出することができます。hello
ディレクトリで以下のコマンドを入力してください。
$ go list -f '{{.Target}}'
C:\Users\xxx\go\bin\hello.exe
それぞれGoのインストール箇所が違うはずですが、上記の出力の場合はC:\Users\xxx\go\bin\
がGoのインストールパスになります。このパスが必要になりますので、メモしてください。
Goのインストールディレクトリを環境変数に追加
環境変数にGoのインストールディレクトリを追加します。そうすることで、パスを指定しなくても、様々なディレクトリから実行ファイルを実行することができます。以下のコマンドを入力してください。
- Linux もしくは Mac
$ export PATH=$PATH:/Users/xxx/go/bin/
- Windows
$ set PATH=%PATH%;C:\Users\xxx\go\bin\
すでに環境変数が存在している場合は、以下のコマンドで変更することができます。
- Linux もしくは Mac
$ go env -w GOBIN=/path/to/your/bin
- Windows
$ go env -w GOBIN=C:\path\to\your\bin
実行ファイルのインストール
環境変数を変更したら実際にインストールしてみましょう。go install
コマンドを実行して、実行ファイルをインストールします。
その後hello
のみをコマンドに入力して、アプリケーションを実行してみましょう。以下の様に挨拶が出力されたらインストールの成功です。
$ hello
map[Darria:Hi, Darria. Welcome! Gladys:Hail, Gladys! Well met! Samantha:Hail, Samantha! Well met!]
まとめ
いかがでしたでしょうか。
ほかのモジュールを使用しながら、いくつかのGoの文法をしようして簡単なモジュールを作成してテストを追加、アプリケーションのコンパイル・インストールまでを行いました。
簡単なモジュールやパッケージを作成できるようになったと思います。ですが、Goにはまだまだたくさんのチュートリアルがあり、様々な機能を持っています。継続して学習を行い、エンジニアとしての成長につなげたいと思います。