Go言語の構造体を使った設計パターン集(DTO・VOなど)
生徒
「Go言語で構造体って使い道がたくさんあるみたいだけど、DTOとかVOって何ですか?」
先生
「DTOやVOは、データを整理して扱いやすくする“設計パターン”の一つです。Goの構造体を使うと、このような設計がとてもシンプルにできますよ。」
生徒
「DTOって難しそう…。初心者でも大丈夫ですか?」
先生
「大丈夫です!わかりやすい例えやコードで、DTOやVOなどの設計パターンを初心者向けにしっかり解説していきますね!」
1. DTOとVOとは何?
まず用語の整理から始めましょう。DTO(Data Transfer Object/データ転送オブジェクト)は、データをまとめて運ぶための“箱”です。画面から受け取った入力やAPIへ返す結果など、ひとかたまりで受け渡ししたい情報を構造体に詰めます。一方、VO(Value Object/値オブジェクト)は、中身そのものに意味とルールを与えた“値”です。「これはメール」「これは金額」のように、型の名前で意図をはっきり示せます。
イメージとしては、返事を入れる封筒=DTOと、その封筒に入っている手紙=VO。封筒は運搬役、手紙は内容の主役です。Goではこの違いを、構造体と独自の型定義でシンプルに表現できます。まずは最小の例で雰囲気をつかみましょう。
package main
import "fmt"
// DTO: 受け渡し用に情報をまとめた箱
type UserDTO struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"` // 今はシンプルにstring
}
// VO: 値そのものに名前を与えて意味を明確化
type Email string
func main() {
// DTOは「まとめて渡す」ために使う
dto := UserDTO{
ID: 1,
Name: "佐藤",
Email: "sato@example.com",
}
// VOは「これはメールだよ」という意図を型名で示す
var mail Email = "sato@example.com"
fmt.Println(dto.Name, dto.Email)
fmt.Println(mail) // Email型だが中身は文字列
}
この例では、UserDTOが「運ぶための箱」、Emailが「意味のある値」です。プログラミング未経験の方は、DTO=まとめて渡す入れ物/VO=中身のラベル付きの値と覚えるだけで十分。まずは名前を付けて区別できることが大切です。細かな作り込みは後の章で少しずつステップアップしていきます。
2. DTOパターン:構造体でデータをまとめる
DTOは、データをまとめて運ぶために使います。次の例では、「ユーザー情報」をAPIなどに渡すためのDTOを構造体で定義しています。
type UserDTO struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
この構造体は、エンコード用や他の関数にまとめて渡すときに便利です。
3. VOパターン:意味のある型を使って表現
VOは、値そのものを型として扱うパターンです。例えば「メールアドレス」を強調したいときに、stringではなくEmailという型を使います。
type Email string
func NewEmail(e string) Email {
// バリデーションなどの処理も追加できる
return Email(e)
}
これにより、「これはメールアドレスですよ」という設計がコードから伝わります。
4. DTOにVOを組み合わせる
DTOとVOは一緒に使うこともできます。次の例では、Email型のVOをDTOの中に入れています。
type UserDTO struct {
ID int `json:"id"`
Name string `json:"name"`
Email Email `json:"email"`
}
こうすることで、メールアドレスはVOで制約や意味を持ちつつ、DTOとしてデータ箱に収められます。
5. Builderパターン風:構造体の初期化をやさしくする
構造体を初期化するSetter風関数を使うと、複雑なデータ構築がやさしくなります。
type UserBuilder struct {
user UserDTO
}
func NewUserBuilder() *UserBuilder {
return &UserBuilder{}
}
func (b *UserBuilder) SetID(id int) *UserBuilder {
b.user.ID = id
return b
}
func (b *UserBuilder) SetName(name string) *UserBuilder {
b.user.Name = name
return b
}
func (b *UserBuilder) SetEmail(email Email) *UserBuilder {
b.user.Email = email
return b
}
func (b *UserBuilder) Build() UserDTO {
return b.user
}
使い方は次のとおり:
user := NewUserBuilder().
SetID(1).
SetName("佐藤").
SetEmail(NewEmail("sato@example.com")).
Build()
6. Repositoryパターン:データのやり取りを分離
実際のDBやファイルとやり取りする部分を構造体とメソッドでまとめると、コードがシンプルになります。
type UserRepo struct {
// DB接続情報など
}
func (r *UserRepo) Save(u UserDTO) error {
// 保存処理
return nil
}
func (r *UserRepo) FindByID(id int) (UserDTO, error) {
// 取得処理
return UserDTO{}, nil
}
このように構造体とメソッドをまとめることで、保存や取得のロジックを見やすくできます。
7. なぜ設計パターンが役立つの?
構造体とDTO、VO、Builder、Repositoryといった設計パターンを使うと、データの流れをわかりやすく整理できるようになります。特に、コードが読みやすくなり、バグも減りやすくなるメリットがあります。
例えばDTOを使えば、他の関数に必要な情報だけを渡すことができ、関数がシンプルになります。
まとめ
ここまで、Go言語で構造体を使いながら、データを整理して扱いやすくするための設計パターンについて学んできました。たとえば、データをまとめてやり取りするためのDTO、意味を持った値として扱うVO、構造体の初期化をわかりやすくするBuilder、そして保存や取得を任せるRepositoryなど、現実のアプリケーションで出てきやすい考え方を少しずつ紹介しました。どれも難しそうな名前ですが、実際にコードを見てみると、「なるほど、こういうことか」と自然に理解できるようになっています。とくにDTOは、他の関数や外部とデータをやり取りするときに役立ち、VOは中身の文字列に意味を与えて、読みやすさや間違いにくさにつながります。
ここで大切なのは、「きれいに整理されたデータは、バグを減らし、理解をやさしくする」ということです。初心者がつまずきやすい部分は、コードが複雑になってくると、どこからどんなデータが渡っているのかがわからなくなってしまうところです。DTOなら専用の箱として整理され、VOなら名前だけで何を意味しているのかが自然に読み取れます。まるで封筒に宛名を書いて情報を渡すような感覚で、プログラムの中をデータが移動してくれます。
さらに、構造体はGo言語でとても大切な仕組みなので、設計パターンを意識しながら使っていくと、ゆっくりと理解が深まり、複雑なコードでも迷わずに読み進められるようになります。とくに、入力や出力が増えたときほど、設計が生きてきます。必要な値だけを箱につめて関数に渡したり、VOで意味のある名前をつけたりすることで、コード全体の「読みやすさ」がぐっと良くなります。
また、サンプルとして出てきたように、Builder風の書き方を使うと、構造体の初期化がわかりやすくなるという効果もあります。たとえば値が増えても一行ずつ積み重ねる形なので、何を設定しているのかが一目でわかり、初心者でも読みやすい形になります。
Repositoryについても、実際のアプリケーションで使われることが多い考え方です。保存や取得をまとめておくことで、データベース処理を書く場所と、ビジネスロジックを書く場所を分けられます。これによって、ひとつの関数に処理が詰まりすぎず、後から読み返しても理解しやすい形になります。ただ、最初に使うときは名前が難しく見えるかもしれません。ですが「保存の担当」と「取得の担当」をひとつの構造体にまとめたもの、と考えれば自然と受け入れやすいでしょう。
最初は「設計パターン」という言葉に身構えてしまいがちですが、ひとつずつゆっくり触れてみると、むずかしい数学のような仕組みではなく、ただ「読みやすく」「整理された」書き方の工夫に過ぎません。はじめてプログラムを書く人でも、DTOを用意して値を詰めることは簡単にできますし、VOに名前を付けることも特別な技術ではありません。「意味のある名前」「必要な値だけを渡す」「読みやすい形にしておく」という、誰でも感じられるポイントを大切にすれば十分です。
ここでもう一度、シンプルなサンプルを振り返ってみましょう。
package main
import (
"encoding/json"
"fmt"
)
type Email string
type UserDTO struct {
ID int `json:"id"`
Name string `json:"name"`
Email Email `json:"email"`
}
func NewEmail(e string) Email {
return Email(e)
}
func main() {
u := UserDTO{
ID: 1,
Name: "佐藤",
Email: NewEmail("sato@example.com"),
}
b, _ := json.Marshal(u)
fmt.Println(string(b))
}
この短いサンプルの中でも、DTOとVOの役割がはっきり分かれています。UserDTOは情報を一まとめにして渡す箱、Emailは「これはメールアドレスですよ」という意味を持った値です。どんな小さなプログラムでも、この考え方を取り入れて書くことで、後から自分で読み返したときに理解しやすくなります。アプリケーションが大きくなると、こうした小さな積み重ねが大きな違いを生みます。
もう少し大きな視点で考えると、このような設計パターンはチーム開発でも役立ちます。誰かが書いた構造体に意味がついていれば、他の人が読んだとき「ああ、これは入力の箱なんだな」「これはメールアドレスなんだな」とすぐに分かります。言葉で説明する必要がなく、型そのものが説明書の役割を果たしてくれます。つまりプログラマ同士の会話がスムーズになり、誤解も減ります。
初心者のうちは「とにかく動けばいい」という気持ちでコードを書くことが多いのですが、少し余裕が出てきたら、こうした設計の考え方を取り入れてみると、ひとつ上のレベルに進めます。いきなり全部取り入れなくても、まずはDTOとVOだけでも十分効果があります。小さな改善を重ねていくうちに、自然と理解が深まり、今後の学習にも繋がります。
仕組みを知っておくことで、たとえば入力チェックをどこに書くか、データの保存はどこで行うかなどがはっきりして、プログラムが迷子になりません。設計パターンは難しい呪文ではなく、迷子にならないための地図のようなものです。地図があると、途中で道を忘れてもすぐに戻れます。何度も書きながら少しずつ身につけていくのが一番の近道です。
これで、Go言語の構造体を使った設計パターンの基本をひととおり学ぶことができました。現場で活躍するエンジニアも、実はこうした基本的な設計を大切にしながら毎日コードを書いています。初心者でも、ひとつずつ真似していけば必ず身につきます。焦らず、楽しく書き続けてみてください。
生徒
「DTOとかVOって、名前を聞いたときはむずかしそうだと思ったけど、こうして見ていくとただの整理術なんですね。」
先生
「そうなんです。箱に入れるのがDTO、意味を持つ値がVO。道具として使い分けるだけで、コードの見通しがよくなります。」
生徒
「設計パターンって聞くと専門的な印象でしたけど、途中のサンプルみたいに、実際のプログラムにそのまま使える形なんですね。」
先生
「はい。特別な仕掛けではなく、読みやすく整理するための工夫です。これからアプリを作るときにも、自然に役立ちますよ。」
生徒
「少しずつ取り入れて、読みやすいコードを目指してみます!」