Goの公式チュートリアルをやってみる【モジュールの作成】

はじめに

前回は環境構築からHello Worldを出力するまでの基本的なアプリを作成しました。
前回の記事はこちら。

Goの公式チュートリアルをやってみる【環境構築と基本】

今回は基本的なことをおさらいしながら、ほかのモジュールから呼び出せる関数を持つ小さなモジュールを作成します。

モジュールの作成

まずはモジュールを作成してみましょう。
モジュールとは1つ以上の関連するパッケージを集めて、便利な関数のセットにしたものです。コードをまとめたのがパッケージ。パッケージをまとめたのがモジュールとなるのです。

まずはgreetingsというフォルダを作成して、go mod init example.com/greetingsをコマンドプロンプトに入力してgreetingsパッケージを作成しましょう。この例ではexample.comとしていますが、モジュールを公開する場合はダウンロードできるパスにしてください。

まずはgreetings.goファイルを作成して、以下のコードを追加しましょう。

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変数に格納して初期化します。

宣言と初期化を分けたい場合は、ほかの言語の様に以下のコードで表すこともできます。

greetings.go
var message string
message = fmt.Sprintf("Hi, %v. Welcome!", name)

fmt.Sprintf関数

fmt.Sprintfを使用すると引数で受け取ったnameを含めて文字列を作成することがあります。今回は"Hi, %v. Welcome!"%vnameの値が入った文字列を返します。

return

他の言語と同様にreturnを記述すると関数はそこで終了し、パラメータを付けることができます。

別のモジュールからコードを使用する

先ほどは挨拶モジュールを作成しました。ここからは先ほど作った挨拶モジュールのHello関数を呼び出すコードを書いていき、アプリケーションを実行できる状態にします。

Helloモジュールの作成

まずはhelloフォルダを作成しましょう。以下のようにhelloフォルダとgreetingsフォルダが同じ階層になるように作成してください。

<home>/
 |-- greetings/
 |-- hello/

これから書くコードの依存関係を追跡するために、依存性トラッキングを有効にします。go mod initコマンドを実行して有効にしましょう。

go mod init example.com/helloコマンドをコマンドプロンプトに入力して追跡できるようにしましょう。前回と同じように公開する場合は、example.comを変更してください。
前回の環境構築のテストで、モジュールをすでに作成している場合は中身を一度削除し、初期化しなおしましょう。

無事初期化ができたら早速hello.goを作成して、以下のコードを追加しましょう。

hello/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コマンドを使用して設定を変更してください。

上記コマンドを実行すると、以下の様に変換する設定が追加されていることを確認できます。

go.mod
<!-- 省略 -->

replace example.com/greetings => ../greetings

設定を書き込んだらgo mod tidyを実行してモジュールの依存関係を整理してみましょう。その後もう一度go.modファイルを開いて中身を確認してみてください。

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モジュールからエラーを返して呼び出し側で正しく処理するようなコードを追加してみましょう。

先ほどは「名前を教えてもらい挨拶を返す関数」を作成しました。しかし、名前が分からなかったらどうでしょう。正しく挨拶を返していると言えるのでしょうか。早速名前が空の場合はエラーを返すようにしてみましょう。

hello/hello.go
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特定の型の変数が値を持たないことを表す。型の情報を持つ。*int32nil*int64nilを比較しても一致しない。
例:error型の変数errには中身が入っていない
nullオブジェクトが存在しないことを表す。型の情報を持たない。*int32のnullと*int64のnullを比較しても一致しない。
例:error型の変数errは参照されておらずデータが存在しない

エラーの受け取り方

Hello関数の戻り値が二つに変更されたので、呼び出し元も変更する必要があります。以下のコードに変更してみましょう。

hello/hello.go
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関数を実行して返ってきた二つの戻り値をmessageerrに代入
  • Hello関数の名前を空文字に変更して、エラー処理を確認できるように変更
  • errnilではない場合エラーとする
  • 標準ライブラリである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つの挨拶メッセージを含む小さなスライスを追加して、ランダムにメッセージの一つを返すように変更します。

greetings/greetings.go
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パッケージから呼び出す際に、名前を入力せずに実行していました。今回はエラーを出したくないため、名前を入力して確認しましょう。

hello.go
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に以下のコードを追加します。

greetings/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や無限ループを表すことが可能です。
forrangeを使用してfor インデックスの変数名, データの変数名 := range 配列もしくはスライスの変数という表し方でforeachを表します。

インデックスは今回使用しないため_空白識別子)を使用して破棄しています。インデックスのみ使用したい場合は、データの変数名省略することもできます。

複数の名前とメッセージフォーマットを取得する

作成したHellos関数を使用して複数の名前を渡し、複数の名前とメッセージフォーマットを取得できるように変更しましょう。

hello/hello.go
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コマンドを使用したテストでのビルドには含めることができます。

greetings/greetings_test.go
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型へのポインタをパラメーターとして受け取ります。このパラメーターのメソッドを使用して、テストからのレポートやロギングを行います
  • テストの実装
    • TestHelloNameHello関数を呼び出して、関数が有効な応答メッセージを返せるような名前の値を渡します。呼び出しがエラーまたは予期しない応答メッセージを返した場合、tパラメーターのFatalIfメソッドを使用してメッセージを出力して、実行を終了させます。
    • TestHelloEmptyHello関数を空の文字列で呼び出します。このテストはエラー処理が機能するかを確認するためのテストです。空ではない文字列を返すか、エラーがなければ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に名前を含めないように変更して実行します。

greetings/greetings.go
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にはまだまだたくさんのチュートリアルがあり、様々な機能を持っています。継続して学習を行い、エンジニアとしての成長につなげたいと思います。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です