七転八起

技術的に気になったことや詰まったことをメモ替わりに書いていきます。

Docker Composeで作るMongoDBレプリカセット

MongoDB 5.0がリリースされましたね🎉🎉🎉

昨年、業務でMongoDBを使用したPythonアプリケーションを作成したのですが、MongoDBは全く触ったことがなく、社内にもほとんど知見がなかったので、ローカル環境を構築するだけでも一苦労でした。あまり設定を変えることがなかったのですが、久しぶりに設定を調整しようとしたら、当時身につけたはずの知識がゴッゾリ抜け落ちていました。MongoDBのセットアップに関する記事、特にDocker Composeを活用するような場合の記事は本当に少なかったので、自分用にメモを残しておきます。

ローカル環境でレプリカセットを作る場合、Homebrewなどでインストールしたmongodで作ろうとすると、設定ファイルを2つ作る必要があり、また1つに戻したい時にまた設定を変えたりしないといけないので、かなり面倒が多いです(経験談)。Docker Composeだとプロジェクトごとに設定を切り分けることができるので、圧倒的にお勧めします。

MongoDBのレプリカセットのドキュメントはこちらです。

MongoDBのインストール

mongodでサーバーを立てることはしないのですが、私はDockerで立てたMongoDBサーバーにローカルからmongoでアクセスしたくなるので、ローカルにもMongoDBをインストールしておきます。公式ドキュメントに各OSごとのインストール方法が載っています。

スタンドアロンのMongoDB

まずはレプリカセットなしのMongoDBの立て方ですが、これは非常に簡単です。

docker-compose.ymlは下記のようになります。

version: "3.9"

volumes: 
  data_primary:
  log_primary:

services:
  mongo-primary:
    image: mongo:5.0
    restart: always
    environment:
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=root
    volumes: 
      - data_primary:/data/db
      - log_primary:/var/log/mongodb
    ports: 
      - "27017:27017"

mongoイメージのドキュメントに記載がありますが、MONGO_INITDB_ROOT_USERNAMEMONGO_INITDB_ROOT_PASSWORDは必ず設定する必要があります。

上記ファイルを作成後、docker compose up -dでmongodを起動できます。起動後、mongo shellに入ることができます。

$ mongo -u root -p root
MongoDB shell version v5.0.0
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("23fd8b07-b217-47f7-8c6d-8964f785bf1f") }
MongoDB server version: 5.0.0
================
Warning: the "mongo" shell has been superseded by "mongosh",
which delivers improved usability and compatibility.The "mongo" shell has been deprecated and will be removed in
an upcoming release.
We recommend you begin using "mongosh".
For installation instructions, see
https://docs.mongodb.com/mongodb-shell/install/
================
---
The server generated these startup warnings when booting:
        2021-07-17T04:55:48.453+00:00: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem
---
---
        Enable MongoDB's free cloud-based monitoring service, which will then receive and display
        metrics about your deployment (disk utilization, CPU, operation statistics, etc).

        The monitoring data will be available on a MongoDB website with a unique URL accessible to you
        and anyone you share the URL with. MongoDB may use this information to make product
        improvements and to suggest MongoDB products and deployment options to you.

        To enable free monitoring, run the following command: db.enableFreeMonitoring()
        To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
---
> 

レプリカセットのMongoDB

Docker Composeを使えばレプリカセットも簡単に作ることができますが、スタンドアロンと比較すると少しステップが増えます。もし、上記のdocker-compose.ymlを書き換える場合は、Docker Volumeに設定が残ってしまうので、消しておきましょう。

# XXXXはディレクトリ名
$ docker volume rm XXXX_data_primary XXXX_log_primary

Keyfile作成

レプリカセットを作成する場合は、Keyfileというファイルを用意する必要があります。これはレプリカセットに含まれる各サーバー間の接続の際の認証のためです。(Keyfileのドキュメント

OpenSSLで鍵を作成し、権限を変更します。

$ openssl rand -base64 756 > key
$ chmod 400 key

docker-compose.yml作成

docker-compose.ymlは下記のようになります。

version: "3.9"

volumes: 
  data_primary:
  log_primary:
  data_secondary:
  log_secondary:

services:
  mongo-primary:
    image: mongo:5.0
    restart: always
    environment:
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=root
    volumes: 
      - data_primary:/data/db
      - log_primary:/var/log/mongodb
      - ./key:/etc/key
    ports: 
      - "27017:27017"
    command: --keyFile /etc/key --replSet rs0

  mongo-secondary:
    image: mongo:5.0
    restart: always
    environment:
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=root
    volumes: 
      - data_secondary:/data/db
      - log_secondary:/var/log/mongodb
      - ./key:/etc/key
    ports: 
      - "27018:27017"
    command: --keyFile /etc/key --replSet rs0

レプリカセットの初期設定

Docker Composeを起動させたら、mongoコマンドでmongo shellに入ります。その中でrs.initiate()でレプリカセットの初期設定を行います。

$ mongo -u root -p root
MongoDB shell version v5.0.0
....
> rs.initiate({
   _id: "rs0",
   members: [
       {_id: 0, host: "mongo-primary:27017"},
       {_id: 1, host: "mongo-secondary:27017"}
   ]
})
{"ok": 1}
rs0:SECONDARY>

{"ok": 1}が返ってきたら設定成功です。shellがrs0:SECONDARY>に変わります。そのサーバーがPRIMARYになるかSECONDARYになるかはガチャのようですが、設定直後は必ずSECONDARYと表示されます。

これでレプリカセットの設定は終わりです。あとは普通にMongoDBを使用する要領でデータの読み書きができます。一方のサーバーに書き込みを行うと、もう一方にも書き込まれます。

DBの接続・切断の実装の共通化

前回の記事の続きです。今のコードの状態はこちらです。

元のコード

getTasksHandler, createTaskHandler, updateTaskHandler, deleteTaskHandlerの4つのCRUD処理をするハンドラ関数がありますが、これらの関数の最初の部分に下記のようなコードが重複して現れています。

func getTasksHandler(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  // この部分が何度も出現している
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

  // DBのデータを用いた処理(省略)
}

ハンドラとロギングの分離」で行ったように関数をチェインして対応したいと思います。

コードの修正

DBと接続した後にデータの処理を行い、それが完了した後にDBとの接続を切断します。このデータの処理の部分が内容によって変わります。なので、この部分を関数として引数に渡して、その関数の前後で接続・切断の処理を行います。言葉で説明するよりもコードを見た方がわかりやすいかと思います。

下記のconnectDatabaseという関数で前述の処理を行います。引数としてdb *pg.DBを引数とする関数を渡します。これは処理の中でdbを必ず使用するからです。それ以外に必要な変数等がある場合は、外側のスコープから渡すようにします。本当は関数を返すようにした方が汎用性が高いような気がしますが、ひとまずこれで良しとします。

type databaseHandler func(*pg.DB)

func connectDatabase(f databaseHandler) {
    db := getDatabase()
    f(db)
    if err := db.Close(); err != nil {
        log.Println(err)
    }
}

4つのハンドラ関数の修正はどれも同じことを行うので、updateTaskHandlerを例にとって修正したコードを見ていきます。updateTaskHandlerの中では、

  1. パスパラメータのidからDBのデータを取得し、task *Taskに格納する
  2. HTTPのBodyで送られてきた更新内容をtaskに格納する
  3. DBのデータを更新する

という処理を行います。これらを*Taskのメソッドにした上で、connectDatabaseの内部で実行します。

func (t *Task) parse(r io.ReadCloser) {
    data, _ := ioutil.ReadAll(r)
    err := json.Unmarshal(data, t)
    if err != nil {
        panic(err)
    }

    if err = r.Close(); err != nil {
        panic(err)
    }
}

func (t *Task) find(db *pg.DB, id uint) {
    if err := db.Model(t).Where("id = ?", id).Select(); err != nil {
        log.Println(err)
    }
}

func (t *Task) update(db *pg.DB) {
    if _, err := db.Model(t).WherePK().Update(); err != nil {
        log.Println(err)
    }
}

func updateTaskHandler(_ http.ResponseWriter, r *http.Request, p httprouter.Params) {
    id, _ := strconv.Atoi(p.ByName("id"))
    task := new(Task)
    connectDatabase(func(db *pg.DB) {
        task.find(db, uint(id))
        task.parse(r.Body)
        task.update(db)
    })
}

最後に

もっといいやり方あるかもしれません。でもこれしか思いつかなかったので、これでよしとします。

マルチプレクサにhttprouterを使用する

前回の記事の続きです。

ここまでサードパーティライブラリはgo-pg/pgしか使用しておらず、HTTPを捌くのにnet/httpしか使っていませんでした。『Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る』で紹介されていたhttprouterがGoでルーティングを捌くのに良さそうだったので、試してみました。

マルチプレクサのライブラリ

上記の書籍にはhttprouterのみ紹介されていましたが、net/httpの流儀に従いつつ、ルーティングを柔軟に実装できるライブラリとして、httprouter以外にgorilla/muxがあったので、両者について簡単に調べてみた。

gorilla/muxはnet/httpのhttp.HandlerFuncをそのまま使えます。なので、元々net/httpで実装していたハンドラを変更しなくても、ルーティングの実装を変更することができます。特徴の一つとして、パスパターンに正規表現を使用できることがあげられます。パスに不適切な値が入ってきたときに、それをルーティングでフィルタできるのは良いですね。

httprouterはハンドラの登録にGETPOSTといったHTTPメソッドと同じ名前の関数を使用することができます。同じパスでもメソッドによってハンドラを切り替えることができ、また、それをわかりやすく明示できるのはよいですね(gorilla/muxでも同じようなことができるようですが)。gorilla/muxとは異なり、正規表現によるフィルタはできないようです。

パフォーマンスについてはこのようなベンチマークがありました(これを作っているのはhttprouterの作者なので、多少ポジショントーク的な部分はありそうですが笑)。パフォーマンスはhttprouterの方がいいようです。今回はhttprouterを使っていきます。

元のコード

少し長いですが、下記のような状態になっています。httprouterを使用するためにはtasksHandlerの中でHTTPメソッドごとに分けている処理をハンドラにする必要があり、それによりtasksHandlerを削除できるようになります。

// 省略

func tasksHandler(w http.ResponseWriter, r *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return
    }

  // この部分をなんとかしたい
    switch r.Method {
    case "GET":
        getTasks(w, db)
    case "POST":
        createTask(w, r, db)
    case "PUT":
        updateTask(r, m, db)
    case "DELETE":
        deleteTask(m, db)
    }
}

func getTasks(w http.ResponseWriter, db *pg.DB) {
    var tasks []Task
    if err := db.Model(&tasks).Select(); err != nil {
        panic(err)
    }

    tasksJson, err := json.Marshal(tasks)
    if err != nil {
        panic(err)
    }

    if _, err = w.Write(tasksJson); err != nil {
        panic(err)
    }
}

func createTask(w http.ResponseWriter, r *http.Request, db *pg.DB) {
    var task Task
    task.parseBody(r)
    if _, err := db.Model(&task).Insert(); err != nil {
        panic(err)
    }

    taskJson, _ := json.Marshal(task)
    if _, err := w.Write(taskJson); err != nil {
        panic(err)
    }
}

func updateTask(r *http.Request, m []string, db *pg.DB) {
    id, _ := strconv.Atoi(m[2])
    task := new(Task)
    task.find(db, uint(id))

    task.parseBody(r)
    if _, err := db.Model(task).WherePK().Update(); err != nil {
        log.Println(err)
    }
}

func deleteTask(m []string, db *pg.DB) {
    id, _ := strconv.Atoi(m[2])
    task := new(Task)
    task.find(db, uint(id))
    if _, err := db.Model(task).WherePK().Delete(); err != nil {
        log.Println(err)
    }
}

// 省略

func main() {
  // この部分をhttprouterで書き換える
    http.HandleFunc("/api/tasks/", accessLog(tasksHandler))
    log.Fatal(http.ListenAndServe(":5000", nil))
}

修正の手順

1. 各処理をhttp.HandlerFuncにする

現状、各処理の関数の引数はバラバラで、各処理に必要な値のみを渡すようにしています。これらの関数をhttp.HandlerFuncの型にして、http.HandleFuncで登録できるようにします。httprouterを使用する場合はhttprouter.Handleという別の型のハンドラ関数にする必要があるのですが、パスパラメータが引数に追加されるだけなので、一旦http.HandlerFuncにします。また、この過程でコードの重複が生まれてしまいますが、そこも一旦目を瞑って進めていきます。この後、各処理をハンドラとして扱うことになるので、関数名も...Handlerという名称に変えています。

// 省略

func tasksHandler(w http.ResponseWriter, r *http.Request) {
  // DBやパスに関する処理を各関数に移行する
  // 引数を(w http.ResponseWriter, r *http.Request)の型に揃える
    switch r.Method {
    case "GET":
        getTasksHandler(w, r)
    case "POST":
        createTaskHandler(w, r)
    case "PUT":
        updateTaskHandler(w, r)
    case "DELETE":
        deleteTaskHandler(w, r)
    }
}

func getTasksHandler(w http.ResponseWriter, _ *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    var tasks []Task
    if err := db.Model(&tasks).Select(); err != nil {
        panic(err)
    }

    tasksJson, err := json.Marshal(tasks)
    if err != nil {
        panic(err)
    }

    if _, err = w.Write(tasksJson); err != nil {
        panic(err)
    }
}

func createTaskHandler(w http.ResponseWriter, r *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    var task Task
    task.parseBody(r)
    if _, err := db.Model(&task).Insert(); err != nil {
        panic(err)
    }

    taskJson, _ := json.Marshal(task)
    if _, err := w.Write(taskJson); err != nil {
        panic(err)
    }
}

func updateTaskHandler(w http.ResponseWriter, r *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return
    }

    id, _ := strconv.Atoi(m[2])
    task := new(Task)
    task.find(db, uint(id))

    task.parseBody(r)
    if _, err := db.Model(task).WherePK().Update(); err != nil {
        log.Println(err)
    }
}

func deleteTaskHandler(w http.ResponseWriter, r *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return
    }

    id, _ := strconv.Atoi(m[2])
    task := new(Task)
    task.find(db, uint(id))
    if _, err := db.Model(task).WherePK().Delete(); err != nil {
        log.Println(err)
    }
}

// 省略

2. httprouterのルーターを使用する

次にmainの内部で行っているルーティングの処理をhttprouterで書き換えていきます。この際に、1で書き換えた関数の引数に_ httprouter.Paramsを追記する必要があります。この引数は後々使用しますが、一旦全てこの形にします。またこれにより、tasksHanlderは削除することができました。

// 省略
// tasksHandlerは削除する

// 引数にhttprouter.Paramsを追加
func getTasksHandler(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  // 省略
}

func createTaskHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    // 省略
}

func updateTaskHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    // 省略
}

func deleteTaskHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    // 省略
}

// 省略

func main() {
    router := httprouter.New()

    // /tasks
    router.GET("/api/tasks", accessLog(getTasksHandler))
    router.POST("/api/tasks", accessLog(createTaskHandler))
    router.PUT("/api/tasks/:id", accessLog(updateTaskHandler))
    router.DELETE("/api/tasks/:id", accessLog(deleteTaskHandler))

    log.Fatal(http.ListenAndServe(":5000", router))
}

3. PUT/DELETEでのidの処理にhttprouter.Paramsを使用する

これまでパスに含まれるTaskのidを正規表現を使用して抽出していましたが、各ハンドラの引数にhttprouter.Paramsが入ったので、それを使用すれば簡単にパスパラメータを処理できるようになります。2でPUT/DELETEのパスに:idが含まれていますが、これを抽出できるようになります。

// 省略

func updateTaskHandler(_ http.ResponseWriter, r *http.Request, p httprouter.Params) {
    // 省略

  // p.Bynameを使用して、パスパラメータを取得
    id, _ := strconv.Atoi(p.ByName("id"))
    task := new(Task)
    task.find(db, uint(id))

  // 省略
}

func deleteTaskHandler(_ http.ResponseWriter, _ *http.Request, p httprouter.Params) {
    // 省略

    id, _ := strconv.Atoi(p.ByName("id"))
    task := new(Task)
    task.find(db, uint(id))

    // 省略
}

// 省略

最終的なコードは下記になります。

https://github.com/kensei18/sample-todo-golang-vue/blob/31a8a4428796df17588804cdc9f9df9f1ea3a20e/backend/main.go

最後に

httprouterを使用することでルーティングが明示的になり、可読性が増したように感じます。ただ、DBに関する処理など、重複したコードが現れたので、その辺りの修正を今後進めていきたいです。

ハンドラとロギングの分離

前回の記事で初めてGoでWebアプリケーションを作成しましたが、コードがかなりごちゃごちゃしているので、いい感じにリファクタリングできないか、試行錯誤していこうと思います。前回のコードの実装が終わって、しばらくGoから離れていたのですが、『Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る』を読み始めて、net/httpを使用したWebアプリケーションのより良い実装方法がわかってきたので、少しずつ試していきたいと思います。

元のコード

下記のように、ハンドラ関数の中にアクセスログのコードを記述していました。

// 省略

// ハンドラ関数
func tasksHandler(w http.ResponseWriter, r *http.Request) {
    // ex) 2021/06/03 19:11:56 GET /api/tasks/
  log.Printf("%v %v", r.Method, r.URL.Path)

  /*
  リクエストを処理する汚いコード
  今後整理したい
  */
}

// 省略

func main() {
    http.HandleFunc("/api/tasks/", tasksHandler)
    log.Fatal(http.ListenAndServe(":5000", nil))
}

このアプリケーションはリソースが1つしかない単純なものなので、上記のようにハンドラ関数の中にアクセスログのロジックを記述したとしても大して問題ありませんが、リソースがさらに増えて、かつ全てのエンドポイントに共通のログを出力させたいとなったときには、各ハンドラ関数にlog.Printf("%v %v", r.Method, r.URL.Path)を記述する必要があり、重複するコードが増えて非常に不便です。上手いやり方ないかな、と思っていましたが、先述の書籍の『3.3.4 ハンドラとハンドラ関数のチェイン』にこれを解決する方法がガッツリ丁寧に載っていました。

修正後のコード

考え方については後回しにして、ひとまずコードを書き換えます。

// 省略

// ハンドラ関数
func tasksHandler(w http.ResponseWriter, r *http.Request) {
  // accessLogに移行

  /*
  リクエストを処理する汚いコード
  今後整理したい
  */
}

// 省略

func accessLog(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%v %v", r.Method, r.URL.Path)
        h(w, r)
    }
}

func main() {
  // ハンドラ関数をaccessLogでチェインにする
    http.HandleFunc("/api/tasks/", accessLog(tasksHandler))
    log.Fatal(http.ListenAndServe(":5000", nil))
}

ログ出力に関するロジックをtasksHandlerから分離して、accessLogという関数に移しています。accessLogはハンドラ関数を引数にとって、ハンドラ関数を返します。内部では無名関数のハンドラ関数を戻り値としていて、その内部ではログ出力をした後に引数としていたハンドラ関数を実行する、という形になっています。実際に書いてみると、どうって事のないシンプルなコードになりました。

メインの処理の前後に別の処理を行うための関数なので、Pythonデコレータと役割的には同じような印象を受けました。ただ、Pythonのデコレータの場合は、デコレータの概念や記法を知らないと書くことができないのはもちろん、人のコードを読んでも何をやっているのかわかりません。そして、@を使った記法はググラビリティが非常に悪いので、「デコレータ」という名称を知らないと、その意味を調べることもできません(体験談)。一方で、上記のように関数をチェインさせる方法は、基本的なプログラミングの考え方を理解している人であれば、多少Goの知識が不足していたとしても十分理解できる、と感じました。もちろんPythonでもデコレータを使わずに記述することは可能ですが、Pythonは複数行にわたって無名関数を使用することはできないので、関数内関数を作り、その関数名をつけて、その内部に処理を書いて、それを外側の関数で返す、というステップを踏まないといけないです(デコレータ使う場合でも同じことをしないといけない)。それと比べると、同じことをやっていても、Goの方がスッキリしているかな、という印象です。

最後に

net/httpにはデフォルトでのログ出力のロジックがないので、自分で実装しないといけないというのは、Ruby on Rails / FastAPIでの開発しかやってきていない身としては新鮮でした。改めてRailsやFastAPIの利便性を認識できたと同時に、その辺の実装を自分でやれば、後からの変更も自分でできるので、柔軟性はこっちの方が高いかな、とも思いました(どっちをとっても面倒はあるはずですが笑)。

初めてのGolang Web Application

Goの基本の基本くらいを勉強し終わったので、簡単なTODOアプリケーションを作ってみました。(公式ドキュメントのWriting Web Applicationsに毛の生えた程度のアプリケーションです笑)

https://github.com/kensei18/sample-todo-golang-vue

Ruby/PythonでしかWeb APIを作ったことがなかったので、これだけしょぼいアプリですが、基本の構文の使い方を調べたりやらライブラリのドキュメント読んだりやらで、トータルで1週間くらいかかってしまいました。アプリケーションの構成はリポジトリのトップに frontend/backend/ を置いて、前者にVue.jsアプリケーション、後者にGoのWeb APIを作る、という形にしました。Goの勉強のために始めたはずなのに、フロントエンドの作成に1日かかってしまいました笑。そして、Goを使っていることをアピールしたいはずなのに、リポジトリのLanguagesを見ると、Vue.jsが53%で、Goは32%でした笑。

f:id:kensei18:20210602091347p:plain

Goを使ってみることが目的だったので、以下ではGoのWeb APIを作成した流れを書いていきます。

新規プロジェクトの作成

go mod で作りました。poetryのようにリポジトリごとにパッケージ管理する機能みたいです。

$ mkdir sample-todo-golang-vue
$ cd sample-todo-golang-vue
$ go init sample-todo-golang-vue

公開して他の人に使ってもらう、というものでもなかったので上記のようにして作成しましたが、ライブラリとして後悔する場合は go init github.com/kensei18/sample-todo-golang-vue のようにするのかな、と思います。正直、このあたりのGoの作法を全く分かっていないので、一度きちんとドキュメント読むなりしないとな、と感じています。

ファイル構成

実際にソースコードを見ていただくのが早いですが、アプリケーションのコードは全てmain.go に記述しています。Goでの良いファイルの分割の仕方もよくわからなかったのと、色々触りながらnet/httpの基本の使い方を知りたかったので、一旦はそこまで気にせずに作りました。Goのアプリケーションのベストプラクティスもいずれ学びたいです。もしかしたらその際に、今回のアプリケーションを書き換えるかもしれないので、自分用に現時点での構成がわかるようにコミットのURLも貼っておきます。

https://github.com/kensei18/sample-todo-golang-vue/tree/3eb7266ac104a1f743389a1afd02d3b2ac68d2f4

docker-compose.yml がありますが、これはローカルでPostgreSQLを立てたくなかったので、そのためだけに使用しています。GoのDockerfile作るときに使うかもしれないです。

Goプロジェクトの構成に関しては下記のようなベストプラクティスがあるようなので、そのうち読みます。

https://github.com/golang-standards/project-layout

使用したライブラリ

外部ライブラリはGoのPostgreSQLのORMのgo-pg/pgのみ使用しました。Webの基本を改めて学び直す、ということもしたかったので、HTTPの処理に関してはnet/httpのみで行い、Webフレームワークは使用していません。

最後に

一応作り終えたものの、構成がぐちゃぐちゃだったり、テストがなかったりと、自分としても物足りないです。ただ、あまりにもGoでのWebアプリ開発に関してわからないことだらけなので、一回何かしらGoの本を読もうかな、と思っています(公式ドキュメントだけでとりあえずのアプリケーション作れるエコシステムもすごいですね)。