実践編
大文字と小文字の違い
- 大文字:外部パッケージにも共有したい関数・変数(interfaceなど)
- = エクスポートされる
- 小文字:該当するパッケージ内でしか使われない関数・変数
- = 隠匿される
リポジトリの構成
- cmd/server
- GoのAPIサーバーの立ち上げや、DI(=依存性注入)などの処理をここにかこう
- handler
- アプリケーション層、リクエスト、レスポンスの処理を記述する
- domain
- userとgroupのモデリング箇所(今回はstruct定義くらいになっちゃう)
- repositoryのIFの定義(=モデルのAPIのI/Oを一覧で定義する)
- usecase
- ビジネスロジックを記述する
- infrastructure
- 永続化の実装を書く(今回はMySQL)
- schema
- 今回はテーブル定義が記述されている
モジュール関連
- modファイルの作成
go mod init <モジュール名>
クエリパラメータの取得
http.Request.URL.Query().Get("パラメータ名")
で取得できる- 複数のクエリパラメータを取得するときは
http.Request.URL.Query()
でmap配列が取得できる
func hoge (w http.ResponseWriter, r *http.Request) {
// 1つだけ取得
user_id := r.URL.Query().Get("user_id")
// 複数取得(queryのvalueは配列になっているので注意)
params := r.URL.Query()
user_id= params["user_id"][0]
複数レコードを取得する
基本方針
- Query、QueryContextを使ってSQL文を発行することで複数レコードを取得できる
- 得られたrowsをfor文で回すことで個々の要素を取り出すことができる
- 個々の要素を
row.Scan()
で取得し、配列にappendすることでarray型として返却できる
// makeで初期化しないと、レスポンスが空の時nullが返却されてしまう
groups := make([]GroupOutput,0)
user_id := r.URL.Query().Get("user_id")
rows, err := db.QueryContext(context.Background(), "SELECT id, name FROM `groups` WHERE user_id = ?", user_id)
// 取得できなかったときは空配列を返す
if err != nil {
j, _ := json.Marshal(&groups)
w.WriteHeader(http.StatusOK)
w.Write(j)
return
}
defer rows.Close()
// rows.Next()はbooleanを返す。次の要素があるときはTrueになる。
// つまり、rowsの要素が全て取り出されるまで無限ループする
for rows.Next() {
var group GroupOutput
if err := rows.Scan(&group.ID, &group.Name); err != nil {
return
}
groups = append(groups, group)
}
j, err := json.Marshal(&groups)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(j)
注意点
- レスポンスの配列はmakeで初期化すること
- ❌:
var groups []GroupOutput
- makeで初期化しないと、レスポンスが空の時nullが返却されてしまう
- nilと空スライスは扱いが違うぞ!!
- ❌:
モジュールのインストール・削除
go mod tidy
go.modファイルが存在するディレクトリ以下にある.goファイルを自動で検知してくれる
他ファイルから関数等をimport
次のようなファイル構成だとする。
practice
├─ cmd/server
├─ ├─ main.go
├─ └─ test.go
├─ pkg_baz
│ └─ baz.go
└─ go.mod
go.mod
module practice
例えば、test.go
からmain.go
内の関数を呼び出したいときは次のようにする。
test.go
package server_test
import(
"testing"
"net/http"
"net/http/httptest"
"practice/cmd/server"
)
func testUserAPI(t *testing.T){
serv := main.関数名()
ドメイン駆動アーキテクチャを取り入れる
基本的な流れ
DDDをモデリングする順序は、次のようにするのが一般的
- エンティティ
- IDを持つオブジェクト
- User、Groupなど
- バリューオブジェクト
- 値や属性
- 住所、名前など
- ドメインサービス
- 純粋な処理のこと
- リポジトリ
- エンティティの状態を管理する
- 状態を管理するための入出力だけを記述する
domain層
- domainではデータのモデリングをする
- Djangoでいうmodels.pyのイメージ
- repositoyではデータ操作時の入力・出力(=インターフェース)を記述する
-
具体的な実装はinfrastracture層に置かれる
-
ポインタについて(参考:https://sgswtky.github.io/post/golang-pointer/)
プリミティブ型(int,stringなど標準である型)
- ポインタを採用するときは少し考えた方がいい
- 値を参照するときにnilチェックをいちいちする必要があるから
構造体
- 構造体のポインタはとにかく使いまくる方がベター
- 構造体のポインタの配列 で扱えばループでも高パフォーマンス
- Golangは変数のデフォルト値が決まっているため、「構造体が無い」という状態が定義できる
- 構造体はメモリを使用する量が多くなりがちなので使ったほうがコスパ良
- 例えば Repositoryから構造体を取得するコードを書く場合に下記のようなインターフェースにしておけば次のメリットがある
- メモリコピーを最小限に抑えられる
- 値ひとつひとつのnilチェックと比較するとコード量が減る ```Go // 使いまくるが良さそうな型 User []User
// そうでない型 *[]User // これは配列のポインタ型
// いい形のインターフェース func (UserRepository) Find(userID string) (User, error) func (UserRepository) FindAll() ([]User, error) ```
Newxxxについて
- Newxxxみたいな記述が多々あるが、これはコンストラクタという
- クリーンアーキテクチャでは、handler→usecase→service→repositoryと言う依存関係があるので、handlerの初期化にはusecaseが、usecaseの初期化にはseriviceが、serviceの初期化にはrepositoryが必要になってくる。
- その際使用するのが各ファイルに実装してあるNewXXXXと言う関数(コンストラクタ)でコンストラクタの返り値はinterfaceにしておくことで、各層をinterfaceで接続でき、疎結合を実現している。