lelelemon’s blog

カメの歩みでのんびり学んでいます。

【Go言語】go/gin で簡単なREST API を作成

Go言語で gin フレームワークを使って REST API を作成する手順について記載しています。

 

実行環境について

下記の環境で動かしています。

  • ubunth "20.04.4 LTS (Focal Fossa) <- WSL2
  • go version go1.21.6 linux/amd64
  • docker: 20.10.17

 

DB環境を準備

今回は PostgreSQL を使用しています。

docker-compose.yml
version: '3'
services:
  postgres:
    image: 'postgres:14'
    container_name: postgres_gin_rest
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: sample
      TZ: Asia/Tokyo
      PGDATA: /var/lib/postgresql/data/pgdata
    ports:
      - '5432:5432'
    volumes:
      - './db-store:/var/lib/postgresql/data'

 

テーブルを作成

docker exec -it postgres_gin_rest psql -U user -d sample

上記で PostgreSQL に接続し、下記のテーブルCREATE文を実行します。

 

CREATE TABLE "user" (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255),
    age INT
);

 

ここで作成したテーブルに対して CRUD を行う REST API を作成していきます。

 

gin を導入

まずは gin を使えるようにします。

go mod init {プロジェクト名}

で go.mod ファイルを作成。

 

main.go を作成し、中身を記載します。

main.go
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/", handleHome)
    r.Run()
}

func handleHome(ctx *gin.Context) {
    ctx.JSON(http.StatusOK, gin.H{"message": "test"})
}

 

下記の import でコンパイルエラーになるので、

github.com/gin-gonic/gin

 

プロジェクトルートで「go mod tidy」コマンドを実行し、gin の依存関係を追加します。

 

go run main.go 
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> main.handleHome (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

 

main.go を実行すると 8080 ポートで起動するので、「http://localhost:8080」にブラウザからアクセスしてみます。

{
    "message": "test"
}

上記が画面に表示されれば無事に接続できています。

 

gin で REST API 構築ー登録処理

続いて、事前に作成した BOOK テーブルに対してデータ登録のエンドポイントを作成していきます。

このエンドポイントは、下記の仕様とします。

  • エンドポイントは http://localhost:8080/user
  • POSTメソッドを受け付け、リクエストボディーで指定された値をもとにUserテーブルにデータを登録する
  • 登録が完了したら、HTTPステータス:200と、登録されたUserレコードの値をレスポンスする
  • リクエストボディーに不備がある場合は、HTTP ステータス:400と、エラー情報をレスポンスする
  • その他想定外のエラーが発生した場合は、HTTP ステータス:500と、エラー情報をレスポンスする

 

まずはBOOKテーブルに該当するモデルクラスを作成します。(model パッケージを作成してその中に model.go ファイルを作成しています)

 

model/model.go
package model

type User struct {
    Id   int
    Name string
    Age  int
}

 

 

続いて、データ登録処理を書きます。(こちらも、funcs パッケージを作成してその中に funcs.go ファイルを作成しています)

 

funcs/funcs.go
package funcs

import (
    "database/sql"
    "fmt"
    "gin-rest/model"
    "log"
)

func InsertUser(db *sql.DB, user model.User) (model.User, error) {
    var insertedUser model.User

    err := db.QueryRow(
        "INSERT INTO public.user (name, age)"+
            "VALUES ($1, $2) RETURNING id, name, age",
        user.Name, user.Age).Scan(
        &insertedUser.Id, &insertedUser.Name, &insertedUser.Age,
    )

    if err != nil {
        log.Fatal("error inserting user")
        fmt.Println(err)
        return model.User{}, err
    }

    return insertedUser, nil
}

 

main.go には下記を追記します。

main.go
func handleCreateUser(ctx *gin.Context) {
    var user model.User

    // リクエストボディからデータを取得
    if err := ctx.ShouldBindJSON(&user); err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user, err := funcs.InsertUser(DB, user)
    if err != nil {
        log.Fatal(err)
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    ctx.JSON(http.StatusOK, gin.H{"user": user})
}
 

 

package main

import (
    "database/sql"
    "gin-rest/funcs"
    "gin-rest/model"
    "log"
    "net/http"

    "github.com/gin-gonic/gin"

    _ "github.com/lib/pq"
)

var DB *sql.DB

func main() {
    initDB()
    r := gin.Default()
    // r.Use(corsMiddleware())
    r.GET("/", handleHome)
    r.POST("/user", handleCreateUser)
    r.Run()
}

 

POST リクエストを受け付け、リクエストボディーから USER テーブルの登録内容を読み取り、データ登録するようにしました。

登録が完了したら、HTTPレスポンスステータス:200と、登録されたデータをレスポンスボディーに含めて返すようにしています。

 

また、下記のimportがコンパイルエラーになるため、

github.com/lib/pq

 

go mod tidy

pq の依存を追加します。

 

それでは、main.go を起動し、POSTリクエストを送信してみます。

 

curl -X POST -H "Content-Type: application/json" -d '{"Name": "User1", "Age": 40}' http://localhost:8080/user
{"user":{"Id":1,"Name":"User1","Age":40}}

 

レスポンスが返ってきたので、PostgreSQL にもつないでデータ登録されているか確認します。

 

sample=# select * from public.user;
 id | name  | age 
----+-------+-----
  1 | User1 |  40
(1 row)

無事に登録できています。

 

gin で REST API 構築ー検索処理

続いて、データ検索のエンドポイントを作成していきます。

このエンドポイントは、下記の仕様とします。

  • エンドポイントは http://localhost:8080/user/{検索したいユーザーID}
  • GETメソッドを受け付け、パスパラメーターで指定されたUserIDをもとにUserテーブルを検索する
  • データが取得ができたら、HTTPステータス:200と、取得されたUserレコードの値をレスポンスする
  • リクエストに不備がある場合は、HTTP ステータス:400と、エラー情報をレスポンスする
  • その他想定外のエラーが発生した場合は、HTTP ステータス:500と、エラー情報をレスポンスする

 

DB 検索処理として、下記を追記します。

funcs/funcs.go
func GetUserByID(db *sql.DB, id int) (model.User, error) {
    row := db.QueryRow("SELECT id, name, age FROM public.user WHERE id = $1", id)

    var user model.User
    err := row.Scan(&user.Id, &user.Name, &user.Age)
    if err != nil {
        log.Fatal("error scanning user")
        return model.User{}, err
    }

    return user, nil
}

 

検索のリクエストをハンドリングできるようにするため、main.go には下記を追記します。

main.go
func handleGetUserByID(ctx *gin.Context) {
    // パスパラメーター "id" を取得
    id := ctx.Param("id")

    // id を整数に変換
    userID, err := strconv.Atoi(id)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
        return
    }

    // GetBookByID 関数を呼び出し
    user, err := funcs.GetUserByID(DB, userID)
    if err != nil {
        log.Fatal(err)
        ctx.JSON(http.StatusInternalServerError,
            gin.H{"error": "Failed to retrieve user"})
        return
    }

    ctx.JSON(http.StatusOK, gin.H{"user": user})
}

 

package main

import (
    "database/sql"
    "gin-rest/funcs"
    "gin-rest/model"
    "log"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"

    _ "github.com/lib/pq"
)

var DB *sql.DB

func main() {
    initDB()
    r := gin.Default()
    r.GET("/", handleHome)
    r.POST("/user", handleCreateUser)
    r.GET("/user/:id", handleGetUserByID)
    r.Run()
}

 

  • ctx.Param でパスパラメーターから値を取得することができます。これは r.GET("/user/:id" の定義箇所になります。
  • strconv.Atio で文字列を数値にパースします。

 

main.go を起動して、http://localhost:8080/user/1 にブラウザからアクセスしてみます。

以下の内容がブラウザ上で確認できると思います。

 

 

 

gin で REST API 構築ー更新処理

続いて、データ更新のエンドポイントを作成していきます。

このエンドポイントは、下記の仕様とします。

  • エンドポイントは http://localhost:8080/user/{更新したいユーザーID}
  • PUTメソッドを受け付け、リクエストボディーで指定された値をもとにUserテーブルの指定IDのデータを更新する
  • 更新が完了したら、HTTPステータス:200と、更新されたUserレコードの値をレスポンスする
  • リクエストボディーに不備がある場合は、HTTP ステータス:400と、エラー情報をレスポンスする
  • その他想定外のエラーが発生した場合は、HTTP ステータス:500と、エラー情報をレスポンスする

DB 更新処理として、下記を追記します。

funcs/funcs.go
func UpdateUser(db *sql.DB, user model.User) (model.User, error) {
    var updatedUser model.User

    err := db.QueryRow(
        "UPDATE public.user SET name=$1, age=$2 WHERE id=$3"+
            " RETURNING id, name, age",
        user.Name, user.Age, user.Id,
    ).Scan(&updatedUser.Id, &updatedUser.Name, &updatedUser.Age)

    if err != nil {
        log.Fatal("error updating user")
        return model.User{}, err
    }

    return updatedUser, nil
}

 

更新のリクエストをハンドリングできるようにするため、main.go には下記を追記します。

main.go
func handleUpdateUser(ctx *gin.Context) {
    // パスパラメーター "id" を取得
    id := ctx.Param("id")

    // id を整数に変換
    userID, err := strconv.Atoi(id)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
        return
    }

    var user model.User

    // リクエストボディからデータを取得
    if err := ctx.ShouldBindJSON(&user); err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // リクエストボディーに含まれるIDを更新対象のIDとして設定
    user.Id = userID

    user, err = funcs.UpdateUser(DB, user)
    if err != nil {
        log.Fatal(err)
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    ctx.JSON(http.StatusOK, gin.H{"user": user})
}

 

func main() {
    initDB()
    r := gin.Default()
    r.GET("/", handleHome)
    r.POST("/user", handleCreateUser)
    r.GET("/user/:id", handleGetUserByID)
    r.PUT("/user/:id", handleUpdateUser)
    r.Run()
}

 

  • ctx.Param でパスパラメーターから更新対象のユーザーIDを取得しています
  • リクエストボディーを読み込み、更新対象のユーザーモデルにマッピングします

 

main.go を起動し、PUTリクエストを送信してみます。

curl -X PUT -H "Content-Type: application/json" -d '{"Name": "Updated User1", "Age": 30}' http://localhost:8080/user/1
{"user":{"Id":1,"Name":"Updated User1","Age":30}}

レスポンスが返ってきたので、PostgreSQL にもつないでデータ登録されているか確認します。

 

select * from public.user;
 id |     name      | age 
----+---------------+-----
  1 | Updated User1 |  30
(1 row)

無事に更新できました。

 

gin で REST API 構築ー削除処理

最後に、データ削除のエンドポイントを作成していきます。

このエンドポイントは、下記の仕様とします。

  • エンドポイントは http://localhost:8080/user/{削除したいユーザーID}
  • DELETEメソッドを受け付け、パスパラメーターで指定されたUserIDに該当するユーザーを削除する
  • データが削除ができたら、HTTPステータス:200をレスポンスする
  • リクエストに不備がある場合は、HTTP ステータス:400と、エラー情報をレスポンスする
  • その他想定外のエラーが発生した場合は、HTTP ステータス:500と、エラー情報をレスポンスする

 

DB 削除処理として、下記を追記します。

funcs/funcs.go
func DeleteUser(db *sql.DB, id int) error {
    existingUser, err := GetUserByID(db, id)
    if err != nil {
        log.Fatal("error deleting user")
        return err
    }

    // ユーザーが存在しない場合はエラーを返す
    if existingUser.Id == 0 {
        return errors.New("user not found")
    }

    // ユーザーが存在する場合は削除を実行
    _, err = db.Exec("DELETE FROM public.user WHERE id = $1", id)
    if err != nil {
        log.Fatal("error deleting user")
        return err
    }

    return nil
}

 

  • 指定IDのユーザーが存在するかDBに問い合わせ、存在する場合に削除処理を実行します

 

削除のリクエストをハンドリングできるようにするため、main.go には下記を追記します。

main.go
func handleDeleteUser(ctx *gin.Context) {
    // パスパラメーター "id" を取得
    id := ctx.Param("id")

    // id を整数に変換
    userID, err := strconv.Atoi(id)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
        return
    }

    err = funcs.DeleteUser(DB, userID)
    if err != nil {
        log.Fatal(err)
        ctx.JSON(http.StatusInternalServerError,
            gin.H{"error": err.Error()})
        return
    }

    ctx.JSON(http.StatusOK, gin.H{})
}

 

func main() {
    initDB()
    r := gin.Default()
    r.GET("/", handleHome)
    r.POST("/user", handleCreateUser)
    r.GET("/user/:id", handleGetUserByID)
    r.PUT("/user/:id", handleUpdateUser)
    r.DELETE("/user/:id", handleDeleteUser)
    r.Run()
}

 

  • ctx.Param でパスパラメーターから削除対象のユーザーIDを取得し、削除処理を実行します

 

main.go を起動し、DELETEリクエストを送信してみます。

 

curl -X DELETE http://localhost:8080/user/1
{}

 

PostgreSQL にもつないでデータ削除されているか確認します。

 

select * from public.user;
 id | name | age 
----+------+-----
(0 rows)

 

無事に削除されました。

 

以上、簡単ですがgo/ginを使ったREST API作成のサンプルです。

gin フレームワークを使用することでシンプルなコードでAPI開発が楽に実装できるのはもちろん、今回は書けていませんがHTMLを返したりもできるので画面開発もできたりします。

似たようなフレームワークEcho もあるので、このあたりも学んでいきたい。

 

 

 

【Go言語】VSCode でデバッグ環境を構築する

開発においてデバッグができるとエラーの特定がしやすいので、

VSCode において Go のデバッグ環境の構築メモを記載します。

 

検証用コード

package main

import "fmt"

func main() {
    a := 1
    b := 2
    c := a + b

    fmt.Println("合計:", c)
}

 

go run main.go 
合計: 3

 

なんでもないシンプルなコードです。

各変数に入る値をデバッグできるようにしてみます。

手順1:デバッグツール(go-delve)を VSCode にインストールする

VSCodeデバッグメニュー(上から4つ目のアイコン)をクリックし、「Run and Debug」をクリックします。

 

すると、VSCode の右下に下記のエラーを示すダイアログが出てきます。

 

どうやら、go-delve というGoの拡張機能が必要なようなので、上記のエラーの指示に従ってインストールします。

Tools environment: GOPATH=/root/go
Installing 1 tool at /root/go/bin
  dlv

Installing github.com/go-delve/delve/cmd/dlv@latest (/root/go/bin/dlv) SUCCEEDED

All tools successfully installed. You are ready to Go. :)

 

手順2:デバッグ実行

インストールできたので、再び「Run and Debug」をクリックします。

 

go.mod がないと怒られたので、「go mod init {プロジェクトフォルダ名}」コマンドを実行して go.mod を作成してから、再度「Run and Debug」をクリックします。

 


エラーが解消して無事に起動できました。

 

続いて、ブレークポイントで特定の場所で処理を止め、変数の値を確認してみます。

10行目の箇所にブレークポイントを貼っています。

ここで処理を一時中断し、変数の値を確認してみます。

 

 

ブレークポイントを貼った状態で、「Run and Debug」をクリックして実行します。

 

すると左上の VARIABLES メニューでブレークポイントで止めた時点の各変数値が確認できます。

ちなみに、F5キーで処理を再開できます。

 

以上、デバッグ環境が構築できました。

 

いままでコマンドラインからの実行のみでコンソールに出力されるエラーを頼りに修正をしていましたが、デバッグできるようになったのでより楽にエラー解消やプログラムの動作確認ができそうです。

 

【Go言語】channel で非同期に並列処理を行い、結果を取得する

Go で時間のかかる処理を並列で実行するのに channel という仕組みがあることを知ったので試しました。

 

channel とは

goroutine間で値を送受信するための機構(ChatGPTより)。

 

 

goroutine は非同期に処理を並行実行できるものの、処理が終了するとそのまま破棄されてしまうため、

非同期処理を行ってレスポンスをやり取りしたい場合は channel を使用する必要があるようです。

 

以降でこの処理を検証します。

 

検証用のコードを用意

func heavyProdess(num int) string {
    time.Sleep(1 * time.Second)
    return fmt.Sprintf("finish %d", num)
}

 

シンプルに、1秒スリープして文字列を返す関数です。

channelを使わない場合(同期)

func main() {
    start := time.Now()
    for i := 0; i < 5; i++ {
        fmt.Println(heavyProdess(i + 1))
    }
    end := time.Now()

    fmt.Println("処理時間:", (end.Sub(start).Seconds()))
}

 

これを実行すると下記の結果です。

go run main.go 
finish 1
finish 2
finish 3
finish 4
finish 5
処理時間: 5.068204847

 

順に同期的に実行していくため、全て実行するのにループ回数分、およそ5秒かかりました。

channelを使う場合(非同期)

続いて、channel を使って処理を並列実行するようにしてみます。

下記のようにコードを書き換えました。

 

func heavyProdess(num int, wg *sync.WaitGroup, ch chan<- string) {
    defer wg.Done()
    time.Sleep(1 * time.Second)
    ch <- fmt.Sprintf("finish %d", num)
}

func main() {
    ch := make(chan string)
    var wg sync.WaitGroup
    start := time.Now()

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go heavyProdess(i+1, &wg, ch)
    }

    go func() {
        wg.Wait()
        close(ch)
    }()

    for result := range ch {
        fmt.Println(result)
    }
    end := time.Now()

    fmt.Println("処理時間:", (end.Sub(start).Seconds()))
}

 

(コードの解説)

WaitGroup を使用して、goroutine をまとめて実行するようにしています。

上記 WaitGroup の説明を抜粋し、ChatGPT で和訳↓

A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.

 

WaitGroup は、一連のゴルーチンの終了を待機します。メインのゴルーチンは Add を呼び出して待機するゴルーチンの数を設定します。その後、各ゴルーチンが実行され、終了時に Done を呼び出します。同時に、Wait を使用してすべてのゴルーチンが終了するまでブロックすることができます。

 

WaitGroup でgoroutineをまとめて並列実行することができるため、

並列実行したgoroutineがそれぞれ channel に値を送信するようにし、最終的に channel から値を取り出す形にしています。

 

heavyProcess 関数
  • defer wg.Done()で WaitGroup の待機カウントを1減らす
  • channel に処理結果を送信
main 関数
  • wg.Add(1) で WaitGroup の待機カウントを1増やす
  • go func 無名関数内の wg.Wait() で WaitGroup のすべての goroutine が完了するのを待ち合わせ、すべて完了したら channel を閉じる
  • for 句で channel から処理結果を取得、表示

これを実行すると下記の結果です。

go run main.go 
finish 5
finish 2
finish 3
finish 4
finish 1
処理時間: 1.001226728

 

同期処理だと約5秒かかっていたのが、channelを使った非同期処理だと約1秒に短縮されました。

 

以上サンプルです。

使いこなせればchannelは強力な武器になるので、ぜひマスターしたいです。

 

【Go言語】gomock で外部へのアクセスをモック化して単体テストを書く

golang単体テストを勉強していて、外部リソースへのアクセスをモック化する方法を調べていたら gomock というのがあることを知り、試したサンプルです。

 

環境準備

mkdir service
mkdir repository
mkdir model
touch service/book_service.go
touch repository/book_repository.go
touch model/book.go

 

tree
.
├── go.mod
├── model
│   └── book.go
├── repository
│   └── book_repository.go
└── service
    └── book_service.go

 

サンプルは以下のような構成にしたいと思います。

  • book_service.go -> book 関連のビジネスロジックを提供。
  • book_repository.go -> book 関連の DB アクセス周りの処理を提供。
  • book.go -> book 構造体定義

 

単体テスト対象のコード

続いてテスト対象コードを用意していきます。

 

book.go
package model

type Book struct {
    Id    int
    Name  string
    Price int
}
book_repository.go
package repository

import "unittest/model"

type BookRepository interface {
    GetBookById(id int) model.Book
}

 

Repository にDB アクセス処理があると仮定して、これを後ほどモックします。

book_service.go
package service

import (
    "unittest/model"
    "unittest/repository"
)

type BookService struct {
    Repository repository.BookRepository
}

func (service *BookService) GetBookById(id int) model.Book {
    return service.Repository.GetBookById(id)
}

 

こちらの Service から、Repository の処理を呼び出します。

 

単体テストコード

mock の生成

まずは repository の mock を作成します。

gomock では、リンク先の README に書かれているように、mockgen コマンドで mock を自動生成できるようです。

 

それでは、先に作成した Repository の mock を作成してみます。

repository フォルダに移動して、下記のコマンドを実行します。

 

mockgen -source book_repository.go -destination mock/book_repository.go

 

すると、mock フォルダが生成され、その中に book_repository.go が生成されます。

.
├── book_repository.go
└── mock
    └── book_repository.go

 

生成された  book_repository.go の下記のimport 句でコンパイルエラーになっているので、

gomock "github.com/golang/mock/gomock"

 

unittest フォルダ直下で下記コマンドを実行して gomock の依存を追加します。

go mod tidy

 

単体テストコード

repository の mock が作れたので、続いて単体テストコードを書きます。

repository を使用する Service の単体テストを書いていきます。

 

package service

import (
    "testing"
    "unittest/model"
    mock_db "unittest/repository/mock"

    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/assert"
)

func TestGetBookById(t *testing.T) {
    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()

    expectedBook := model.Book{Id: 100, Name: "テストブック", Price: 1500}

    mockObj := mock_db.NewMockBookRepository(mockCtrl)
    mockObj.EXPECT().GetBookById(100).Return(expectedBook)

    service := BookService{Repository: mockObj}

    actualBook := service.GetBookById(100)

    assert.Equal(t, expectedBook, actualBook)
}

 

テストの検証を簡潔にするため、github.com/stretchr/testify/assert を使用しています。

コンパイルエラーになると思うので、

go mod tidy

で assert の依存関係を追加します。

 

テストコードの大まかな実装の参考は gomockリファレンス より。

先に生成した mock を、mock_db としてインポートしています。

mockObj.EXPECT().{テスト対象関数}.RETURN({戻り値})

上記の形式で戻り値を指定できます。

 

その後、上記で設定した mock を、Service のパラメーターとして下記のように渡し、Service のメソッドを呼び出してテストを実行します。

service := BookService{Repository: mockObj}

 

go test
PASS
ok      unittest/service        0.004s

 

テストがPASSしました。

 

ちゃんと設定した内容が mock に反映されているか確認するため、わざと失敗のテストケースにしてみます。

 

func TestGetBookById(t *testing.T) {
    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()

    expectedBook := model.Book{Id: 100, Name: "テストブック", Price: 1500}

    mockObj := mock_db.NewMockBookRepository(mockCtrl)
    mockObj.EXPECT().GetBookById(100).Return(expectedBook)

    service := BookService{Repository: mockObj}

    actualBook := service.GetBookById(200)

    assert.Equal(t, expectedBook, actualBook)
}

 

go test
--- FAIL: TestGetBookById (0.00s)
    book_service.go:13: Unexpected call to *mock_repository.MockBookRepository.GetBookById([200]) at /root/repos/go-samples/unittest/service/book_service.go:13 because: 
        expected call at /root/repos/go-samples/unittest/service/book_service_test.go:19 doesn't match the argument at index 0.
        Got: 200 (int)
        Want: is equal to 100 (int)
    controller.go:269: missing call(s) to *mock_repository.MockBookRepository.GetBookById(is equal to 100 (int)) /root/repos/go-samples/unittest/service/book_service_test.go:19
    controller.go:269: aborting test due to missing call(s)
FAIL
exit status 1
FAIL    unittest/service        0.005s

 

テストが失敗することが確認できました。

 

以上サンプルになります。

 

参考

qiita.com

【Go言語】DB接続サンプル(PostgreSQL)

golangでDB接続(PostgreSQL)を試したのでそのサンプルです。

 

以下サンプル。

golangPostgreSQL に接続

PostgreSQL を準備

まずは接続先のPostgreSQL環境を準備します。

ここは手っ取り早く、Docker で環境を作りました。

(前提)

Docker がインストール済みであること

postgresql がインストール済みであること(psql コマンドを使用して DB接続を確認するため)

下記の docker-compose.yml を作成し、「docker-compose up -d」で起動します。

 

[docker-compose.yml]

version: '3'

services:
  postgres:
    image: postgres:14
    container_name: postgres
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: sample
      TZ: "Asia/Tokyo"
      PGDATA: /var/lib/postgresql/data/pgdata
    ports:
      - 5432:5432
    volumes:
      - ./db-store:/var/lib/postgresql/data

 

docker exec -it postgres psql -U user -d sample

上記コマンドでPostgreSQLに接続ができます。

これでPostgreSQL環境構築は完了です。

docker exec -it postgres psql -U user -d sample
psql (14.10 (Debian 14.10-1.pgdg120+1))
Type "help" for help.

sample=# 

 

Goプログラムから PostgreSQLに接続

続いて、Go でPostgreSQL に接続する処理を書いていきます。

pq ライブラリを使用しました。

 

下記で各種ファイルを用意します。

mkdir sample

go mod init sample

touch main.go

 

まずは main.go には下記の内容を記載します。

 

package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq"
)

func main() {
    connStr := "user=user password=password dbname=sample sslmode=disable"
    _, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("DB接続")
}

 

「_ "github.com/lib/pq"」のところでコンパイルエラーになるので、

go mod tidy

を実行することで、pq が go.mod の依存関係に追加されます。(go.mod ファイルが更新される)

 

ここまでできたら、main.go を実行します。

「DB接続」と表示されれば、問題なく接続ができています。

go run main.go 
DB接続

 

PostgreSQLへのCRUD処理を追加

接続するだけだと面白くないので、DBへのCRUDも試します。

 

まずは動作検証用のテーブルを用意。

CREATE TABLE "user" (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255),
    age INT
);

 
PostgreSQLへデータ登録

続いてデータの登録です。

データ登録用の dbInsert 関数を追加しています。

 

package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq"
)

func main() {
    connStr := "user=user password=password dbname=sample sslmode=disable"
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("DB接続")

    // コマンドラインからの入力を受け付ける
    var command string
    fmt.Print("コマンドを入力 (insert/update/delete/select): ")
    fmt.Scan(&command)

    switch command {
    case "insert":
        dbInsert(db)
    default:
        fmt.Println("無効なコマンド")
    }
}

func dbInsert(db *sql.DB) {
    var name string
    var age int

    fmt.Print("名前を入力: ")
    fmt.Scan(&name)

    fmt.Print("年齢を入力: ")
    fmt.Scan(&age)

    // INSERT文を実行
    result, err := db.Exec("INSERT INTO \"user\" (name, age) VALUES ($1, $2)", name, age)
    if err != nil {
        log.Fatal(err)
    }

    // 追加成功時のメッセージと追加されたレコード内容を表示
    fmt.Println("追加しました。")
    lastInsertID, _ := result.LastInsertId()
    fmt.Printf("追加されたレコードのID: %d, 名前: %s, 年齢: %d\n", lastInsertID, name, age)

}

 

コマンドラインから、insert/update/delete/select のいずれかの入力を受け付け、

insert の場合にデータ登録を行うようにしています。

これを実行してみます。

go run main.go 
DB接続
コマンドを入力 (insert/update/delete/select): insert
名前を入力: test
年齢を入力: 10
追加しました。
追加されたレコードのID: 0, 名前: test, 年齢: 10

 

PostgreSQLにもSelect句を投げてみます。

select * from public.user;
 id | name | age 
----+------+-----
  1 | test |  10
(1 row)

 

無事に登録できています。

 

PostgreSQLへデータ更新

続いてデータの更新です。

データ更新用の dbUpdate 関数を追加しています。

 

func main() {
    connStr := "user=user password=password dbname=sample sslmode=disable"
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("DB接続")

    // コマンドラインからの入力を受け付ける
    var command string
    fmt.Print("コマンドを入力 (insert/update/delete/select): ")
    fmt.Scan(&command)

    switch command {
    case "insert":
        dbInsert(db)
    case "update":
        dbUpdate(db)
    default:
        fmt.Println("無効なコマンド")
    }
}

func dbUpdate(db *sql.DB) {
    var id int
    var name string
    var age int

    fmt.Print("更新対象のレコードのIDを入力: ")
    fmt.Scan(&id)

    fmt.Print("名前を入力: ")
    fmt.Scan(&name)

    fmt.Print("年齢を入力: ")
    fmt.Scan(&age)

    // UPDATE文を実行
    result, err := db.Exec("UPDATE \"user\" SET name = $1, age = $2 WHERE id = $3", name, age, id)
    if err != nil {
        log.Fatal(err)
    }

    // 更新成功時のメッセージと更新されたレコード内容を表示
    rowsAffected, _ := result.RowsAffected()
    if rowsAffected > 0 {
        fmt.Println("更新しました。")
        fmt.Printf("更新されたレコードのID: %d, 新しい名前: %s, 新しい年齢: %d\n", id, name, age)
    } else {
        fmt.Println("指定されたIDのレコードが見つかりませんでした。")
    }
}

 

コマンドラインからの入力が、update の場合にデータ更新を行うようにしています。

これを実行してみます。

go run main.go 
DB接続
コマンドを入力 (insert/update/delete/select): update
更新対象のレコードのIDを入力: 1
名前を入力: test_update
年齢を入力: 20
更新しました。
更新されたレコードのID: 1, 新しい名前: test_update, 新しい年齢: 20

 

PostgreSQLにもSelect句を投げてみます。

select * from public.user where id = 1;
 id |    name     | age 
----+-------------+-----
  1 | test_update |  20
(1 row)

 

無事に更新できています。

 

PostgreSQLへデータ検索

続いてデータの検索です。

データ検索用の dbSelect 関数を追加しています。

 

func main() {
    connStr := "user=user password=password dbname=sample sslmode=disable"
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("DB接続")

    // コマンドラインからの入力を受け付ける
    var command string
    fmt.Print("コマンドを入力 (insert/update/delete/select): ")
    fmt.Scan(&command)

    switch command {
    case "insert":
        dbInsert(db)
    case "update":
        dbUpdate(db)
    case "select":
        dbSelect(db)
    default:
        fmt.Println("無効なコマンド")
    }
}
 
func dbSelect(db *sql.DB) {
    var id int

    fmt.Print("検索対象のレコードのIDを入力: ")
    fmt.Scan(&id)

    // SELECT文を実行
    row := db.QueryRow("SELECT id, name, age FROM \"user\" WHERE id = $1", id)

    var selectedID int
    var selectedName string
    var selectedAge int

    // 取得したレコードの値をスキャン
    err := row.Scan(&selectedID, &selectedName, &selectedAge)
    if err != nil {
        if err == sql.ErrNoRows {
            fmt.Println("指定されたIDのレコードが見つかりませんでした。")
        } else {
            log.Fatal(err)
        }
        return
    }

    // 取得したレコードの内容を表示
    fmt.Printf("ID: %d, 名前: %s, 年齢: %d\n", selectedID, selectedName, selectedAge)
}

 

コマンドラインからの入力が、select の場合にデータ検索を行うようにしています。

これを実行してみます。

go run main.go 
DB接続
コマンドを入力 (insert/update/delete/select): select
検索対象のレコードのIDを入力: 1
ID: 1, 名前: test_update, 年齢: 20

 

PostgreSQLにもSelect句を投げてみます。

select * from public.user where id = 1;
 id |    name     | age 
----+-------------+-----
  1 | test_update |  20
(1 row)

 

無事に検索できています。

 

PostgreSQLへデータ削除

続いてデータの削除です。

データ削除用の dbDelete 関数を追加しています。

 

func main() {
    connStr := "user=user password=password dbname=sample sslmode=disable"
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("DB接続")

    // コマンドラインからの入力を受け付ける
    var command string
    fmt.Print("コマンドを入力 (insert/update/delete/select): ")
    fmt.Scan(&command)

    switch command {
    case "insert":
        dbInsert(db)
    case "update":
        dbUpdate(db)
    case "select":
        dbSelect(db)
    case "delete":
        dbDelete(db)
    default:
        fmt.Println("無効なコマンド")
    }
}
 
func dbDelete(db *sql.DB) {
    var id int

    fmt.Print("削除対象のレコードのIDを入力: ")
    fmt.Scan(&id)

    // DELETE文を実行
    result, err := db.Exec("DELETE FROM \"user\" WHERE id = $1", id)
    if err != nil {
        log.Fatal(err)
    }

    // 削除成功時のメッセージを表示
    rowsAffected, _ := result.RowsAffected()
    if rowsAffected > 0 {
        fmt.Println("削除しました。")
    } else {
        fmt.Println("指定されたIDのレコードが見つかりませんでした。")
    }
}

 

コマンドラインからの入力が、delete の場合にデータ削除を行うようにしています。

これを実行してみます。

go run main.go 
DB接続
コマンドを入力 (insert/update/delete/select): delete
削除対象のレコードのIDを入力: 1
削除しました。

 

PostgreSQLにもSelect句を投げてみます。

select * from public.user where id = 1;
 id | name | age 
----+------+-----
(0 rows)

 

無事に削除できています。

 

以上サンプルになります。

 

【Go言語】DB接続サンプル(MySQL)

golangでDB接続(MySQL)を試したのでそのサンプルです。

 

以下サンプル。

golangMysql に接続

MySQL を準備

まずは接続先のMySQL環境を準備します。

ここは手っ取り早く、Docker で環境を作りました。

(前提)

Docker がインストール済みであること

mysql-client がインストール済みであること(mysql コマンドを使用して DB接続を確認するため)

下記の docker-compose.yml を作成し、「docker-compose up -d」で起動します。

 

[docker-compose.yml]

version: '3'
services:
  # MYSQL
  db:
    image: mysql:8.0.23
    ports:
      - "3306:3306"
    container_name: mysql_host
    environment:
      MYSQL_ROOT_PASSWORD: mysql
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./db/data:/var/lib/mysql
      - ./db/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./db/sql:/docker-entrypoint-initdb.d

 

mysql -h 127.0.0.1 --port 3306 -uroot -pmysql

上記コマンドでMySQLに接続ができます。

これでMySQL環境構築は完了です。

mysql -h 127.0.0.1 --port 3306 -uroot -pmysql
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.23 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql

 

Goプログラムから MySQLに接続

続いて、Go でMySQL に接続する処理を書いていきます。

 Go-MySQL-Driver ライブラリを使用しました。

 

下記で各種ファイルを用意します。

mkdir sample

go mod init sample

touch main.go

 

まずは main.go には下記の内容を記載します。

 

package main

import (
    "database/sql"
    "fmt"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "root:mysql@/sample")

    if err != nil {
        panic(err)
    }

    // See "Important settings" section.
    db.SetConnMaxLifetime(time.Minute * 3)
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(10)

    fmt.Println("DB接続")
}

 

github.com/go-sql-driver/mysql」のところでコンパイルエラーになるので、

go mod tidy

を実行することで、go-sql-driver が go.mod の依存関係に追加されます。(go.mod ファイルが更新される)

 

MySQLには事前にsampleデータベースを作っておきます。(「CREATE DATABASE IF NOT EXISTS sample;」 を実行)

show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sample             |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

 

ここまでできたら、main.go を実行します。

「DB接続」と表示されれば、問題なく接続ができています。

go run main.go 
DB接続

 

MySQLへのCRUD処理を追加

接続するだけだと面白くないので、DBへのCRUDも試します。

 

まずは動作検証用のテーブルを用意。

use samle;

CREATE TABLE user (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255),
    age INT
);

 
MySQLへデータ登録

続いてデータの登録です。

データ登録用の dbInsert 関数を追加しています。

 

package main

import (
    "database/sql"
    "fmt"
    "log"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "root:mysql@/sample")

    if err != nil {
        panic(err)
    }

    // See "Important settings" section.
    db.SetConnMaxLifetime(time.Minute * 3)
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(10)

    fmt.Println("DB接続")

    // コマンドラインからの入力を受け付ける
    var command string
    fmt.Print("コマンドを入力 (insert/update/delete/select): ")
    fmt.Scan(&command)

    switch command {
    case "insert":
        dbInsert(db)
    default:
        fmt.Println("無効なコマンド")
    }
}

func dbInsert(db *sql.DB) {
    var name string
    var age int

    fmt.Print("名前を入力: ")
    fmt.Scan(&name)

    fmt.Print("年齢を入力: ")
    fmt.Scan(&age)

    // INSERT文を実行
    result, err := db.Exec("INSERT INTO user (name, age) VALUES (?, ?)", name, age)
    if err != nil {
        log.Fatal(err)
    }

    // 追加成功時のメッセージと追加されたレコード内容を表示
    fmt.Println("追加しました。")
    lastInsertID, _ := result.LastInsertId()
    fmt.Printf("追加されたレコードのID: %d, 名前: %s, 年齢: %d\n", lastInsertID, name, age)
}

 

コマンドラインから、insert/update/delete/select のいずれかの入力を受け付け、

insert の場合にデータ登録を行うようにしています。

これを実行してみます。

go run main.go 
DB接続
コマンドを入力 (insert/update/delete/select): insert
名前を入力: test
年齢を入力: 10
追加しました。
追加されたレコードのID: 1, 名前: test, 年齢: 10

 

MySQLにもSelect句を投げてみます。

select * from user;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | test |   10 |
+----+------+------+
1 row in set (0.00 sec)

 

無事に登録できています。

 

MySQLへデータ更新

続いてデータの更新です。

データ更新用の dbUpdate 関数を追加しています。

 

func main() {
    db, err := sql.Open("mysql", "root:mysql@/sample")

    if err != nil {
        panic(err)
    }

    // See "Important settings" section.
    db.SetConnMaxLifetime(time.Minute * 3)
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(10)

    fmt.Println("DB接続")

    // コマンドラインからの入力を受け付ける
    var command string
    fmt.Print("コマンドを入力 (insert/update/delete/select): ")
    fmt.Scan(&command)

    switch command {
    case "insert":
        dbInsert(db)
    case "update":
        dbUpdate(db)
    default:
        fmt.Println("無効なコマンド")
    }
}

func dbUpdate(db *sql.DB) {
    var id int
    var name string
    var age int

    fmt.Print("更新対象のレコードのIDを入力: ")
    fmt.Scan(&id)

    fmt.Print("新しい名前を入力: ")
    fmt.Scan(&name)

    fmt.Print("新しい年齢を入力: ")
    fmt.Scan(&age)

    // UPDATE文を実行
    result, err := db.Exec("UPDATE user SET name = ?, age = ? WHERE id = ?", name, age, id)
    if err != nil {
        log.Fatal(err)
    }

    // 更新成功時のメッセージと更新されたレコード内容を表示
    rowsAffected, _ := result.RowsAffected()
    if rowsAffected > 0 {
        fmt.Println("更新しました。")
        fmt.Printf("更新されたレコードのID: %d, 新しい名前: %s, 新しい年齢: %d\n", id, name, age)
    } else {
        fmt.Println("指定されたIDのレコードが見つかりませんでした。")
    }
}

 

コマンドラインからの入力が、update の場合にデータ更新を行うようにしています。

これを実行してみます。

go run main.go 
DB接続
コマンドを入力 (insert/update/delete/select): update
更新対象のレコードのIDを入力: 1
新しい名前を入力: test_update
新しい年齢を入力: 20
更新しました。
更新されたレコードのID: 1, 新しい名前: test_update, 新しい年齢: 20

 

MySQLにもSelect句を投げてみます。

select * from user where id = 1;
+----+-------------+------+
| id | name        | age  |
+----+-------------+------+
|  1 | test_update |   20 |
+----+-------------+------+
1 row in set (0.00 sec)

 

無事に更新できています。

 

MySQLへデータ検索

続いてデータの検索です。

データ検索用の dbSelect 関数を追加しています。

 

func main() {
    db, err := sql.Open("mysql", "root:mysql@/sample")

    if err != nil {
        panic(err)
    }

    // See "Important settings" section.
    db.SetConnMaxLifetime(time.Minute * 3)
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(10)

    fmt.Println("DB接続")

    // コマンドラインからの入力を受け付ける
    var command string
    fmt.Print("コマンドを入力 (insert/update/delete/select): ")
    fmt.Scan(&command)

    switch command {
    case "insert":
        dbInsert(db)
    case "update":
        dbUpdate(db)
    case "select":
        dbSelect(db)
    default:
        fmt.Println("無効なコマンド")
    }
}
 
func dbSelect(db *sql.DB) {
    var id int

    fmt.Print("検索対象のレコードのIDを入力: ")
    fmt.Scan(&id)

    // SELECT文を実行
    row := db.QueryRow("SELECT id, name, age FROM user WHERE id = ?", id)

    var selectedID int
    var selectedName string
    var selectedAge int

    // 取得したレコードの値をスキャン
    err := row.Scan(&selectedID, &selectedName, &selectedAge)
    if err != nil {
        log.Fatal(err)
    }

    // 取得したレコードの内容を表示
    fmt.Printf("ID: %d, 名前: %s, 年齢: %d\n", selectedID, selectedName, selectedAge)
}

 

コマンドラインからの入力が、select の場合にデータ検索を行うようにしています。

これを実行してみます。

go run main.go 
DB接続
コマンドを入力 (insert/update/delete/select): select
検索対象のレコードのIDを入力: 1
ID: 1, 名前: test_update, 年齢: 20

 

MySQLにもSelect句を投げてみます。

select * from user where id = 1;
+----+-------------+------+
| id | name        | age  |
+----+-------------+------+
|  1 | test_update |   20 |
+----+-------------+------+
1 row in set (0.00 sec)

 

無事に検索できています。

 

MySQLへデータ削除

続いてデータの削除です。

データ削除用の dbDelete 関数を追加しています。

 

func main() {
    db, err := sql.Open("mysql", "root:mysql@/sample")

    if err != nil {
        panic(err)
    }

    // See "Important settings" section.
    db.SetConnMaxLifetime(time.Minute * 3)
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(10)

    fmt.Println("DB接続")

    // コマンドラインからの入力を受け付ける
    var command string
    fmt.Print("コマンドを入力 (insert/update/delete/select): ")
    fmt.Scan(&command)

    switch command {
    case "insert":
        dbInsert(db)
    case "update":
        dbUpdate(db)
    case "select":
        dbSelect(db)
    case "delete":
        dbDelete(db)
    default:
        fmt.Println("無効なコマンド")
    }
}
 
func dbDelete(db *sql.DB) {
    var id int

    fmt.Print("削除対象のレコードのIDを入力: ")
    fmt.Scan(&id)

    // DELETE文を実行
    result, err := db.Exec("DELETE FROM user WHERE id = ?", id)
    if err != nil {
        log.Fatal(err)
    }

    // 削除成功時のメッセージを表示
    rowsAffected, _ := result.RowsAffected()
    if rowsAffected > 0 {
        fmt.Println("削除しました。")
    } else {
        fmt.Println("指定されたIDのレコードが見つかりませんでした。")
    }
}

 

コマンドラインからの入力が、delete の場合にデータ削除を行うようにしています。

これを実行してみます。

go run main.go 
DB接続
コマンドを入力 (insert/update/delete/select): delete
削除対象のレコードのIDを入力: 1
削除しました。

 

MySQLにもSelect句を投げてみます。

mysql> select * from user where id = 1;
Empty set (0.00 sec)

 

無事に削除できています。

 

以上サンプルになります。

 

【Go言語】A Tour Of Go (日本語版) のローカル実行エラー

この記事について

日本語版の A Tour Of Go  に記述のある通り、下記のコマンドでチュートリアルを実行しようとしたところ、

go tool tour

 

下記のエラーとなり、そのときの対応メモです。

go tool tour
go: no such tool "tour"

 

対応内容

同ページ内に下記の手動インストールの記載があったので、まずはこれを試してみた。

上記のコマンドの実行に問題がある場合は手動でこのツアーをインストールして実行できます:

go get github.com/atotto/go-tour-jp/gotour
gotour

 

すると今度は下記のエラー。

go get github.com/atotto/go-tour-jp/gotour
go: go.mod file not found in current directory or any parent directory.
        'go get' is no longer supported outside a module.
        To build and install a command, use 'go install' with a version,
        like 'go install example.com/cmd@latest'
        For more information, see https://golang.org/doc/go-get-install-deprecation
        or run 'go help get' or 'go help install'.

 

go install を使えとのことですが、「example.com/cmd@latest」の部分に何を指定すれば良いかわからず。

英語版の A Tour Of Go には下記が記載されていたので、これを実行してみました。

go install golang.org/x/website/tour@latest

 

これだとうまくダウンロードされ、カレントフォルダに「go」フォルダがダウンロードされ、

その中の bin フォルダに tour という実行可能ファイルが入っているので、

これを実行することでブラウザからアクセスできるようになるようです。

 

./tour
2024/01/01 20:26:13 Serving content from /root/go/pkg/mod/golang.org/x/website/tour@v0.0.0-20210616181959-e0d934b43647
2024/01/01 20:26:13 Please open your web browser and visit http://127.0.0.1:3999
2024/01/01 20:26:25 accepting connection from: 127.0.0.1:47806

 

試しに「http://127.0.0.1:3999/」にアクセス。

チュートリアルができるようになった。。

 

参考情報

macでa tour of goをローカルで実行したときに詰まった #Mac - Qiita