🎃

golangのpackage分割

2023/08/04に公開

サーバーサイドのアプリケーションを作る時にフラットパッケージで作っていたがpackageを切りたくなってきたので、少しメモ。

基本的にモジュールを作りたい場合は基本的にはパッケージルートに置いておくだけですむ。
github.com/somebody/awesome_module というモジュールをimportして使いたい場合はawsome_moduleディレクトリ直下にまとまっていた方が何かと都合がよい。

これに対しアプリケーションを作っていてそれなりの規模になってくると、packageを切りたくなるが循環参照(cyclic import)問題がつきまとう。

「実用Go言語」の 6.4.2 パッケージを階層化する に3パターン書いてあるが具体例はなかった。チーム内で検討してみるといいのかもしれない。

1 共通要素のパッケージを作り、依存先をそこに集中させる
2 ルートパッケージのロジックをすべて移動し、ルートパッケージを共通要素の置き場とする 
3 共通部分を持たない末端のロジックを子パッケージとして切り出す

以下は自分の考えた具体例

├── cipher
│   └── cipher.go
├── go.mod
├── main.go
├── model
│   ├── converter
│   │   └── converter.go
│   ├── db
│   │   └── user.go
│   └── user.go
└── repository
    └── database
        └── database.go

cipher/cipher.go

package cipher

import "encoding/base64"

func Encrypt(value string) string {
	return base64.StdEncoding.EncodeToString([]byte(value))
}

func Decrypt(value string) (string, error) {
	decoded, err := base64.StdEncoding.DecodeString(value)
	return string(decoded), err
}

main.go

package main

import (
	"fmt"
	"main/model"
	"main/model/converter"
	"main/repository/database"
)

func main() {
	{
		dbUser := database.NewUser("taro", "taro@sample.com", string(model.UserStatusActive))
		fmt.Printf("dbUser: %#v\n", dbUser)
		modelUser, err := converter.ConvertToModelUser(dbUser)
		if err != nil {
			fmt.Printf("error: %s\n", err)
		}
		fmt.Printf("modelUser: %#v\n", modelUser)
		fmt.Println(modelUser.IsActive())
	}
	fmt.Println()
	{
		modelUser := model.User{
			Name:   "taro",
			Email:  "taro@sample.com",
			Status: model.UserStatusInactive,
		}
		fmt.Printf("modelUser: %#v\n", modelUser)
		fmt.Println(modelUser.IsActive())
		dbUser := converter.ConvertToDbUser(modelUser)
		fmt.Printf("dbUser: %#v\n", dbUser)
	}

}

model/converter/converter.go

package converter

import (
	"main/cipher"
	"main/model"
	"main/model/db"
)

func ConvertToModelUser(u db.User) (*model.User, error) {
	rawEmail, err := cipher.Decrypt(u.Email)
	if err != nil {
		return nil, err
	}
	return &model.User{
		Name:   u.Name,
		Email:  rawEmail,
		Status: model.UserStatus(u.Status),
	}, nil
}
func ConvertToDbUser(u model.User) db.User {
	return db.User{
		Name:   u.Name,
		Email:  cipher.Encrypt(u.Email),
		Status: string(u.Status),
	}
}

model/db/user.go

package db

type User struct {
	Name   string
	Email  string
	Status string
}

model/user.go

package model

type UserStatus string

const (
	UserStatusActive   UserStatus = "active"
	UserStatusInactive UserStatus = "inactive"
)

type User struct {
	Name   string
	Email  string
	Status UserStatus
}

func (u *User) IsActive() bool {
	return u.Status == UserStatusActive
}

repository/database/database.go

package database

import (
	"main/cipher"
	"main/model/db"
)

func NewUser(name, email, status string) db.User {
	return db.User{
		Name:   name,
		Email:  cipher.Encrypt(email),
		Status: status,
	}
}

実行結果

% go run main.go
dbUser: db.User{Name:"taro", Email:"dGFyb0BzYW1wbGUuY29t", Status:"active"}
modelUser: &model.User{Name:"taro", Email:"taro@sample.com", Status:"active"}
true

modelUser: model.User{Name:"taro", Email:"taro@sample.com", Status:"inactive"}
false
dbUser: db.User{Name:"taro", Email:"dGFyb0BzYW1wbGUuY29t", Status:"inactive"}

「ルートパッケージを共通要素の置き場とする」ならmodel/converter/converter.goはルートに持っていっていいのかもしれない。(main.gomain/main.goみたいに移動する)

Discussion