プログラミング初心者がアーキテクトっぽく語る

見苦しい記事も多数あるとは思いますが訂正しつつブログと共に成長していければと思います

GoのConcurrency

Goと言えばConcurrencyですね。 実際、JavaやPythonのような言語と比べるとハードルが非常に低いと感じました。

安全に使うにはもっと学ばないといけないことが多いと思いますが、とりあえず基本的な部分だけ記載したいと思います。


goroutine

goroutineとはGoにおける軽量スレッドのようなものです。 (厳密にはスレッドではなく、1スレッド上で大量のgoroutineが動作する) main functionもgoroutineです。 go function名()で新しいgoroutineを開始することができます。

package main

import (
    "fmt"
)

func doSomething() {
    fmt.Println("doSomething start")
}

func main() {
    go doSomething()
    // time.Sleep(1 * time.Second)
}

上記の例を実行するとなにも出力されません。 開始したdoSomething goroutineがメッセージを出力するより先にmainが終了してしまうからです。

最後のsleep行のコメントアウトを外せば出力が確認できますが、ダサいです。 次のchannelを使う方法がGoらしい同期方法です。

このようにGoのConcurrencyではmainが他のgoroutineより先に終わらないように(=他のgoroutineの終了を待つように)注意しましょう。


channel

channelはGoのConcurrencyの中核的な要素です。 チャネルを使いこなしましょう。

メッセージ送受信

channelを使ってgoroutine間でデータを送受信できます。 共有リソースにアクセスさせることを避け、channelでデータを交換することがGoの思想です。

channelはmakeで作成します。 このときchannelの型を指定します。

ch := make(chan string)

channelへのデータ送信は以下のようにします。

ch <- "This is a result from doSomething"

channelからデータを受信するには以下のようにします。

result := <-ch

以下がdoSomething goroutineが処理結果をmainへ送っている例です。

func doSomething(ch chan string) {
    fmt.Println("doSomething start")
    ch <- "This is a result from doSomething"
}

func main() {
    ch := make(chan string)
    go doSomething(ch)
    fmt.Println("Received from channel:", <-ch)
}

以下のように複数のgoroutineに同じchannelを渡してデータを複数回受け取る場合は、受け取る回数だけ<-chします。

func doSomethingA(ch chan string) {
    ch <- "This is a result from doSomethingA"
}

func doSomethingB(ch chan string) {
    ch <- "This is a result from doSomethingB"
}

func main() {
    ch := make(chan string)

    go doSomethingA(ch)
    go doSomethingB(ch)

    fmt.Println("Received from channel:", <-ch)
    fmt.Println("Received from channel:", <-ch)
}

Blocking

<-chは、データを受信するまでBlocking状態になります。 これを利用してwait/notifyのようなことができます。 (逆もまた然りでch <- dataは受信側が準備できるまでBlock状態になる)

以下は最初の「goroutine」のサンプルを改善した例です。 main functionの末尾の<-chはdoSomethingがchannelにデータを書き込むまで待ち状態になります。 これでsleepを使わなくてもdoSomethingがメッセージを出力するまでmainの終了を待たせることができます。 このケースではchannel上で送受信するデータはなんでも構わないので空のstructにしてメモリを節約しています。

func doSomething(ch chan struct{}) {
    fmt.Println("doSomething start")
    ch <- struct{}{}    // 終了をmainに通知
}

func main() {
    ch := make(chan struct{})
    go doSomething(ch)
    <-ch      // doSomethingが終了するまで待つ
}

channel buffering

前述の通り、受信側(<-ch)がいないchannelに対するwriteはBlocking状態になります。 以下のようにすると指定した分だけバッファできるようになります。 指定した数を超えるとやはりBlockingが発生します。

 ch := make(chan struct{}, 3)

受信側がいないchannelにwriteするとblockingしないでエラーが起きる?

試してみたらこんなエラーが出て、blocking状態にならないことがあります。

fatal error: all goroutines are asleep - deadlock!

これはgoroutineが2つ以上、ないからです。 <-chする可能性があるgoroutineが1つもないのでGoがdeadlockと判定しています。 goroutineが開始されてないか、開始したけど終了してしまったかのどちらかです。

<-chしなくてもいいのでgoroutineを開始し、time.Sleepなどで数秒間活かしておくとBlocking状態を見ることができます。

range

for文でrangeが使えます。 書き込み側で最後にcloseすることを忘れるとエラーになるので注意しましょう。

func doSomething(ch chan string) {
    fmt.Println("doSomething start")
    ch <- "result 1"
    ch <- "result 2"
    close(ch)
}

func main() {
    ch := make(chan string)
    go doSomething(ch)
    for msg := range ch {
        fmt.Println("Received from channel:", msg)
    }
}

close

channeをcloseすることは必須ではありません。 上述のrangeを利用するときや、もう書き込むものがないことを受信側へ明示的に伝えたいときはcloseします。

closeは書き込み側が行います。 closeすると全ての受信側にメッセージが送られます。 これをnotifyAllのように利用することもできます。

書き込み側が送信直後にcloseしても、close後、最後に書き込まれたデータを受信側が受信することは可能です。


select

selectを使うと複数のchannelがある状況で、最初にデータを受信したchannelを処理することができます。 time.Afterはchannelを返すので組み合わせるとタイムアウト管理もできます。

func doSomethingA(ch chan string) {
    time.Sleep(500 * time.Millisecond)
    ch <- "done"
}

func main() {
    ch := make(chan string)

    go doSomethingA(ch)

    timer := time.After(1 * time.Second)
    select {
    case msg := <-ch:
        fmt.Println("received:", msg)
    case <-timer:
        fmt.Println("Timeout occurred")
    }
}

selectを活用することで色々な仕組みを実装できます。


Atomic counters

sync/atomicモジュールにatomicな操作ができるカウンタがあります。 複数のgoroutineで共有するカウンタが必要な場合はまずはこれを検討します。 シンプルなことはシンプルに実装しましょう。

func main() {
    var counter atomic.Uint64
    counter.Add(2)
    fmt.Println("counter:", counter.Load())
}

Mutex

MutexはJavaやPythonのsynchronizedです。 共有リソースへのアクセスが避けらず、かつAtomicな仕組みが利用できない場合に利用します。 できれば設計レベルで避けたいですね。

この例ではsomeCommonDataへのアクセスを保護しています。

type CommonResource struct {
    lock           sync.Mutex
    someCommonData int
}

func (cr *CommonResource) Add(number int, name string) {
    cr.lock.Lock()
    fmt.Println("Lock started for worker ", name)
    defer cr.lock.Unlock()
    cr.someCommonData += number
    time.Sleep(1 * time.Second)
    fmt.Println("Lock ended for worker ", name)
}

func worker(cr *CommonResource, name string, ch chan struct{}) {
    cr.Add(1, name)
    ch <- struct{}{}
}

func main() {
    ch := make(chan struct{})
    cr := &CommonResource{}
    go worker(cr, "A", ch)
    go worker(cr, "B", ch)

    <-ch
    <-ch
}

このようにアクセスが直列化されました。

❯ go run .
Lock started for worker  B
Lock ended for worker  B
Lock started for worker  A
Lock ended for worker  A
❯ 

wait group

全てのgoroutineの終了を待つ機能です。 シンプルに書けますが、workerからエラーをmainへ伝搬させるのが困難なことが難点です。

func worker(id int) {
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(1 * time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
        wg.Go(func() {
            worker(i)
        })
    }
    wg.Wait()
}

感想

他の言語だと単純な処理を並列に動かすだけでも、色々なコードを書く必要があります。 Goでは普通のFunctionをgo function名()するだけで並列実行できるので、ともかく参入の敷居が低いです。 そこにchannelとselectを組み合わせれば多くのことを実現できます。 GoのConcurrencyが優れていると言われる理由がわかります。

GoでREST APIを叩く(実践編)

前回の方法ではHeaderの指定ができません。 Goで重要な役割を担うContextも使えません。

Goでは基本的にhttp.GET, http.POSTは使いません。 ここでは実践的なREST APIの叩き方を見ていきます。

Context

まずContextについて見ていきましょう。

Contextとは

ContextはGoの汎用的なキャンセル&タイムアウト機能です。 Ctrl-c等、ユーザから中断要求があった場合や、タイマー満了時に処理を中断することができます。 goroutineやHTTP処理の管理に使います。 REST APIを叩くときはContextを利用するのがPracticeだそうです。

Contextの種類

Contextは複数種類ありますが代表的なものにキャンセル機能のみのものと、タイムアウト機能+キャンセル機能を持っているものがあります。 前者はcontext.WithCancel、後者はcontext.WithTimeoutで作成します。

手前のコードでContextを作成する

GETやPOSTを実行する場所でContextを作成することは非推奨です。 その手前のコードで作成し、Injectionして渡します。

親子関係

Contextは親子関係を持つことができます。 親Contextがキャンセルされると子Contextもキャンセルされます。

Contextの使い方

以上のことを総合すると以下のようにするとよいみたいです。

  • 根本のコードで、ユーザ中断要求時等にキャンセルできる親Contextを作って渡す
  • GETやPOSTの手前のコードで上述Contextを親に持つ、タイムアウト機能付きの子Contextを作成して渡す
  • GETやPOSTは渡されたタイムアウト付きの子Contextを使って実行する

コード

実際のコードを見てみましょう。 今回はGETを取り上げますが、POSTもほとんど同じです。

import文

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

根本のコード

今回はこの関数を根本のコードに見立てます。

func runGetWithContext() error {
    fmt.Println("runGetWithContext()")

    parentCtx, cancel := context.WithCancel(context.Background())
    defer cancel()

    todo, err := getTodo(parentCtx)
    if err != nil {
        return fmt.Errorf("failed to get todo: %w", err)
    }
    fmt.Println("Todo:", todo)

    return nil
}

4行目のcontext.Background()で空のContextを作成して、これを最上位のContextにします。

同じ4行目のcontext.withCancelで親Contextを作成します。 親Contextの親は最上位の空Contextです。 2つ目の戻り値cancelを使っていつでも処理を安全に中断することができます。 defer cancel()するのがPracticeです。

7行目で親ContextをgetTodoに渡してます。

GET/POSTの手前のコード

これが実際にGETやPOSTを実施するコードの一歩手前のコードです。

func getTodo(parentCtx context.Context) (*TodoItem, error) {
    ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second)
    defer cancel()

    bodyBytes, err := doGet(ctx, "https://jsonplaceholder.typicode.com/todos/1")
    if err != nil {
        return nil, fmt.Errorf("failed to get todo: %w", err)
    }
    var todoItem TodoItem
    err = json.Unmarshal(bodyBytes, &todoItem)
    if err != nil {
        return nil, fmt.Errorf("failed to unmarshal todo: %w", err)
    }
    return &todoItem, nil
}

2行目のcontext.WithTimeoutでタイムアウト付きのContextを作成してます。 タイムアウトは10秒に設定されています。

このContextの親は渡されたparentCtxです。 parentCtxがキャンセルされと、このContextもキャンセルされます。

5行目でこのContextがdoGetに渡されています。 引数が複数ある場合、通常、Contextは第1引数にします。

GET/POSTを実施するコード

これが最後のコードです。 ここでGETを実行します。

func doGet(ctx context.Context, url string) ([]byte, error) {
    request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, fmt.Errorf("failed to create GET request: %w", err)
    }
    request.Header.Set("Accept", "application/json")
    client := &http.Client{}
    response, err := client.Do(request)
    if err != nil {
        return nil, fmt.Errorf("failed to make GET request: %w", err)
    }
    defer response.Body.Close()

    if response.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("server sent status code: %d", response.StatusCode)
    }

    body, err := io.ReadAll(response.Body)
    if err != nil {
        return nil, fmt.Errorf("failed to read response body: %w", err)
    }
    return body, nil
}

2行目のhttp.NewRequestWithContextでContextからRequestを作成しています。 POSTの場合、第2引数がhttp.MethodPost、第4引数に*bytes.Reader型のJSONデータを入れます。

6行目のrequest.Header.Setでヘッダを設定しています。

7行目でclientを作成して8行目でclient.Doでリクエストを送信しています。

受信したResponseの処理方法は前回の記事と同じです。

GoでREST APIを叩く(基本編)

GoはAPI周りで使うことが多い気がします。 そこでREST APIを叩いてみましょう。

まずは超基本的な使い方を見てみます。 Contextを利用した実践的な使い方は次回、記載する予定です。


単純なGET

GET

import

最低限必要なのはnet/httpです。 Responseを読み取るためにioencoding/jsonも必要です。

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

GET

http.Get1発です。

net/httpにはresponse.BodyをCloseするという作法があります。 必ず実行されるようdeferするのがPracticeです。

 response, err := http.Get("https://jsonplaceholder.typicode.com/todos/1")
    if err != nil {
        return fmt.Errorf("failed to make GET request: %w", err)
    }
    defer response.Body.Close()

Status Code確認

Status Codeはこんな感じで確認できます。

 if response.StatusCode != http.StatusOK {
        return fmt.Errorf("server sent status code: %d", response.StatusCode)
    }

Body確認1:io.ReadAll編

response.Bodyは*http.http2gzipReader型で、このままでは読めません。 ちょっと面倒な手順を踏みます。

まずio.ReadAllで[]byte型にします。

 responseBody, err := io.ReadAll(response.Body)
    if err != nil {
        return fmt.Errorf("failed to read response body: %w", err)
    }

データを展開するstructを定義します。 structの構造はResponse Bodyの構造と一致させ、フィールド名は大文字始まりにします。 行末のjson:xxxはそのフィールドがResponse Bodyのどの項目名とマッチするか指定するものです。 例えばstructのUserIDフィールドはResponse Body内のuserIdとマッチします。

 type Todo struct {
        UserID    int    `json:"userId"`
        ID        int    `json:"id"`
        Title     string `json:"title"`
        Completed bool   `json:"completed"`
    }

最後にjson.Unmarshalを使って、structに展開します。

 var todo Todo
    err = json.Unmarshal(responseBody, &todo)
    if err != nil {
        return fmt.Errorf("failed to unmarshal JSON: %w", err)
    }

ここからはただのstructとして扱えます。

 fmt.Printf("todo title: %+v\n", todo.Title)

この方法はio.ReadAllの結果(=responseBody)を出力できるので、デバッグに便利です。 出力内容を見ながらstructを書いて、最後にUnmarshalする、といった探索的なアプローチとも相性がいいです。 しかしResponseのサイズが大きい場合、メモリ負荷が重くなる点がデメリットです

Body確認2:json.NewDecoder編

先ほど、同様にstructを定義します。 内容は同じです。

 type Todo struct {
        UserID    int    `json:"userId"`
        ID        int    `json:"id"`
        Title     string `json:"title"`
        Completed bool   `json:"completed"`
    }

json.NewDecoderを使って、structに展開します。

 var todo Todo
    err = json.NewDecoder(response.Body).Decode(&todo)
    if err != nil {
        return fmt.Errorf("failed to decode JSON: %w", err)
    }

以降はただのstructとして扱えます。

この方法はストリーミングを使うため、メモリ負荷が軽い点がメリットです。 しかしDecodeが終わるまでBodyの中身が見えない点がデメリットです。


単純なPost

次はPOSTです。

JSONデータの作成

まずはMapを作成します。

 postData := map[string]any{
        "user": map[string]any{
            "attributes": map[string]any{
                "name": "someUserName",
                "pwd":  "someStrongPassword",
            },
        },
    }

次にjson.Marshalで[]byte型にします。

 postJsonData, _ := json.Marshal(postData)

POST

最後にhttp.Postします。 このときJSONデータを*bytes.Readerに変換して渡します。 このためimport文にbytesを追加する必要があります。 response.Bodyは必ずCloseしましょう。

 response, err := http.Post("http://example.com/api/login", "application/json", bytes.NewReader(postJsonData))
    if err != nil {
        return fmt.Errorf("failed to make POST request: %w", err)
    }
    defer response.Body.Close()

戻ってきたResponseの処理方法はGETと同じです。


オレオレ証明書確認スキップ

curl -kに相当する機能です。 怪しい証明書のVerifyをSkipして受け入れます。 セキュリティ上、好ましくありませんが、現実的に身近にこういったマシンがあることは多いです。

先ほどのPOSTの例を取ると、POST部分がこのようになります。 import文にcrypto/tlsを追加する必要があります。

 tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    client := &http.Client{Transport: tr}

    response, err := client.Post("https://example.com/api/login", "application/json", bytes.NewReader(postJsonData))

GETの場合は作成したclientでclient.GETとするだけです。


感想

Pythonのrequestモジュールと比べるとちょっと面倒ですね。 公式ページもPythonと比較するとサンプルが少なめな印象です。 「とりえあえず動かしたい」という時はPythonに軍配が上がると思います。

Goのロギング

fmt.Printlnを連発しないよう、ロギングもできるようになりたいものです。


基本

  • log/slogモジュールを使う
    • 標準のlogはERROR、INFO、DEBUGなどで出力レベルや表示レベルを分けることができない
    • log/slogはJavaやPythonのloggingに近い使い方ができる
  • 根本コードでloggerを生成して各インスタンスへインジェクションするのが一般的
  • 出力先が画面の場合はStderrにする
    • 通常のメッセージはStdoutに出す
  • 小さいアプリの場合はDefaultLoggerを使うのもあり

loggerを作る

  • JSON形式の構造化ログを生成するロガーを作成
    • 最近はこういう要件が増えているらしい
    • 従来のログ形式がいい場合はslog.NewJSONHandlerslog.NewTextHandlerに変更する
func getLogger(logLevel string) *slog.Logger {
    var level slog.Leveler
    switch logLevel {
    case "DEBUG":
        level = slog.LevelDebug
    case "INFO":
        level = slog.LevelInfo
    default:
        level = slog.LevelInfo
    }
    handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
        Level: level,
    })
    return slog.New(handler)
}
  • 戻ってきたloggerをDependency Injectionで各インスタンスへ渡す
  • loggerはlogger.Debug(), logger.Info()のように使う

伝搬してきたエラーを構造化ログに出力する

 if err != nil {
        slog.Error("failed to do something", "error", err)
    }

DefaultLoggerを使う

  • Default Loggerを設定する
 slog.SetDefault(getLogger(os.Getenv("LOG_LEVEL")))
  • 以降はslog.Debug(), slog.Info()のように使える
  • 影響範囲が大きいので普通の開発では避ける
  • 小さいアプリではこれで十分の場合もある

Goのエラー処理

Goで理解が難しかったことの1つがExceptionがないことです。

今回はエラー処理について見ていきましょう。

基本

  • GoにExceptionはない
  • エラーは戻り値で返す
  • 戻り値が複数ある場合は最後の戻り値で返す
  • エラーメッセージを大文字で始めない、末尾に句点をつけない
  • 一番根本のコードで、適切なメッセージを画面に出力する
  • errorsモジュールを使う

エラーを生成する

 if len(results) == 0 {
        return errors.New("result contains no data")
    }

伝搬してきたエラーをさらに伝搬させる

 if err != nil {
        return fmt.Errorf("failed to do something: %w", err)
    }

根本コードで出力する

 if err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }

感想

慣れるとこちらの方がExceptionよりもシンプルかもしれません。 でも時々、Runtime Exceptionが恋しくなります。

Goの基本的な構造

Goで最も理解に苦労したのがクラスがないことです。 最初はクラスがない世界の構造というものがなかなかイメージできませんでしたね。

今回はGoの構造について見ていきましょう。


クラス?

定義

  • クラスはない
  • クラス相当のものはstructをReceiverに持つFunction(メソッド)を作成することで作成する
  • ReceiverはGoの構造体のフィールドやメソッドにアクセスする仕組みでメソッド名の左側に書く
    • 下記サンプル7行目の(p Person)、12行目の(p *Person)
    • JavaのthisやPythonのselfに相当
type Person struct {
    Age    int
    Height int
    Weight int
}

func (p Person) Greet(msg string) error {
    fmt.Println("Hello", msg)
    return nil
}

func (p *Person) UpdateAge(newAge int) {
    p.Age = newAge
}
  • フィールドが不要な場合は空のstructを作ることもできる
type Service struct{}

func (s *Service) DoSomething() string {
    return "something"
}

メソッドの呼び出し

  • メソッドを使うときはこんな風に使う
  • Goが各メソッドのRecieverを見て、8行目を自動的に(&p).UpdateAge(40)に変換してくれる
    • UpdateAgeメソッドの中ではpの複製ではなくp本体のAgeを変更する
func main() {
    p := Person{
        Age:    30,
        Height: 170,
        Weight: 55,
    }
    p.Greet("from Person")
    p.UpdateAge(40)
    fmt.Println("Person's age after UpdateAge:", p.Age)
}
  • 実行結果
Hello from Person
Person's age after UpdateAge: 40

Receiverの型

  • Receiverはポインタである必要はないが、ポインタにした方がJavaやPythonのメソッドの挙動に近い
    • ポインタでない場合、メソッドからフィールドを変更(Ageの変更等)してもその結果が残らない
    • フィールドを変更するメソッドはPointer Receiverにすること(UpdateAge)
    • フィールドを変更しないならValue Receiverでも構わない(Greet)
  • 上記サンプルのようにValue Receiver(p Person)とPointer Receiver(p *Person)が混在しても十把一絡げに扱える
    • 本来はp := Person{}ならp.Greet(),(&p).UpdateAge() とする
    • 逆にp := &Person{}なら(*p).Greet(),p.UpdateAge() とする
    • しかし実際ににはいずれのケースでもp.Greet(),p.UpdateAge() とすればGoが適宜変換してくれる

Interface

  • Interfaceは実装して欲しいメソッドを記載し、それをstruct側で実装するだけ
type Life interface {
    Greet() error
}
  • struct PersonはGreet()メソッドを実装しているのでLifeとして扱える
func main() {
    var life Life
    life = Person{
        Age:    30,
        Height: 170,
        Weight: 55,
    }
    life.Greet("from Life")
}
  • なお、以下のようにinterfaceをポインタにするとエラーになる
    • structの場合は以下のような状況でもメソッドを参照できるが、Interfaceではそれができない
    • 特にDependency Injectionする時に間違えやすいので注意
func useLife() {
    var life *Life
    life = &Person{
        Age:    30,
        Height: 170,
        Weight: 55,
    }
    life.Greet("from Life")
}

継承?

Struct Embedding

  • Goには継承はない
  • 代りにStruct Embeddingがある
  • Struct Embeddingは「Compositionなんだけど継承っぽい特性も付与される」というイメージ

コード

  • struct PersonをEmbedしたstruct Studentを作る
  • このサンプルではvalueだがポインタをEmbedすることもできる
type Student struct {
    Person
    School string
    Grade  int
}
  • 初期化するときはEmbedしたstruct Personの構造を意識する必要がある
 student := Student{
        Person: Person{
            Age:    15,
            Height: 160,
            Weight: 50,
        },
        School: "ABC High School",
        Grade:  9,
    }
  • 使うときはstruct Personを意識しなくてもPersonのフィールドやメソッドにアクセスできる
 msg := fmt.Sprintf("from Student in %d grade at %s", student.Grade, student.School)
    student.Greet(msg)
  • Personを明示的につけてアクセスすることもできる
 student.Person.Greet(msg)
  • StudentはPerson経由でGreet()を持っているのでLifeとして扱うことができる
 var life Life
    life = Student{
        Person: Person{
            Age:    18,
            Height: 180,
            Weight: 60,
        },
        School: "XYZ High School",
        Grade:  12,
    }
    msg := fmt.Sprintf("from Student in %d grade at %s", life.(Student).Grade, life.(Student).School)
    life.Greet(msg)

より大きな構造化単位

ソースファイル

  • xxx.goなどの個々のファイル

パッケージ

  • 1つのディレクトリにまとめられたソースファイル群

モジュール

  • 1つ以上のパッケージから構成される成果物
  • 配布やimportの単位になる

Entrypoint

  • mainパッケージのmain.goソースファイルのmain functionがEntrypoint
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

自前パッケージ置き場

緩くやる場合

  • 公開する予定がない小規模なアプリならプロジェクトフォルダのルート階層に全部置いてもOK

厳密にやる場合

  • 公開したくない内部パッケージはinternal/の下に置く
    • internal/ 以下のパッケージは、その internal/ の親ディレクトリをルートとするツリー内からしか import できない
    • つまり外部のモジュールからは import 不可
  • 外部から import されてもよいパッケージはpkg/の下に置く

感想

Goはシンプルな言語と言われますが、これだけ色々な手段があれば複雑なアプリケーションも作れそうですね。

Goの単体試験

コードを書くときは単体試験も欲しいですね。

テストコード用ファイル作成

  • テスト対象と同じフォルダに<テスト対象>_test.goを作成
myapp_test.go

package名

  • テスト対象と同じpackageにする
package mypkg

モジュールをimport

import (
    "testing"

    "github.com/stretchr/testify/assert"
)
  • testingは必須
  • assertは必須ではないがあった方がAssertしやすい
    • assertがない場合、if actual != expected { t.Errorf(...) }のようなコードを量産することになる

テスト関数作成

  • 関数名はTestから始める
    • 具体的な命名はプロジェクト依存
    • Test<Test対象関数>_<Test内容>などとすると読みやすいかも
  • 引数はt *testing.T
func TestExecute_ItCallsXxxAndYyy(t *testing.T) {
    mock := &MockConnection{}
    useCase := NewUseCase(mock)
    err := useCase.Execute()
    assert.Nil(t, err)
    assert.Equal(t, 1, mock.XxxCalledCount)
    assert.Equal(t, 1, mock.YyyNamesCalledCount)
}

実行

go test ./...
  • 項目ごとの情報を知りたい場合は以下のようにする
go test -v ./...