はじめに
「学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025」14日目の記事は、Goのコードをより本格的に、そして綺麗に整理するための設計手法に踏み込みます。
これまでの記事では、APIの処理をmain.goにベタ書きしてきましたが、アプリケーションが大きくなるにつれて、このままでは見通しが悪くなるのは明らかです。
かといって、Goにはclassがないので、どうやってデータをまとめるのかはずっと疑問でした。
Go言語にはstructに振る舞いを持たせる メソッド(レシーバ) と、それらの依存関係を疎にするインターフェースの基本的な考え方を使って、オブジェクト指向に似せたプログラムを書くことができます。
- Goでシンプルな関数は書けるが、コードの整理や設計に悩んでいる方
- JavaやPythonなど、他のオブジェクト指向言語の経験があり、Go流の設計思想を知りたい方
- テストしやすいコードの書き方に興味があるバックエンド初学者
これまで作ってきたシンプルなWeb APIを、これらの考え方を使ってより構造化された、テストしやすい設計にリファクタリングするハンズオンを交えながら、Goのメソッドとインターフェースの役割を理解し、それらを使ってWeb APIを構造化できるように、頑張りましょう!
Go言語における「オブジェクト指向」?
まず、Javaのようなオブジェクト指向言語の特徴を振り返ってみましょう。
- クラス (Class): データ(フィールド)と振る舞い(メソッド)をまとめた設計図。
- 継承 (Inheritance): あるクラスの性質を別のクラスが引き継ぐ仕組み。
一方で、Go言語にはクラスも継承もありません。
その代わりに、「継承より合成 (Composition over Inheritance)」という設計思想を重視し、以下のシンプルな要素を組み合わせてオブジェクト指向的なプログラミングを実現します。
-
struct(構造体): 関連するデータをまとめるためのカスタム型、クラスのフィールド定義に似ています -
method(メソッド):structに紐付けられた関数、structに振る舞いを与えます -
interface(インターフェース): メソッドのシグネチャ(名前、引数、戻り値)の集まり
Goでは、これらの部品を「合成」することで、柔軟で疎結合な設計を構築していきます。
具体例:レシーバとメソッドの導入
structに振る舞い(メソッド)を持たせるために使われるのがレシーバです。
funcキーワードと関数名の間に、 (変数名 型名) の形で記述します。
package main
import "fmt"
// 挨拶をする人、というデータ構造を定義
type Greeter struct {
Message string
}
// Greeter構造体をレシーバに持つ、Greetメソッドを定義
func (g Greeter) Greet() string {
return g.Message
}
func main() {
// Greeterのインスタンスを作成
greeter := Greeter{Message: "Hello, Method!"}
// メソッドを呼び出す
fmt.Println(greeter.Greet()) // -> "Hello, Method!"
}
func (g Greeter) Greet() string の (g Greeter) の部分がレシーバ記述部分です。
Goの関数の構文は、func Greet() stringという感じですから、funcとGreet()の間にstructを指定することにより、レシーバとして認識させます。
これにより、Greet関数はGreeter型の専用の関数、すなわちメソッドとなり、greeter.Greet()のように呼び出すことができます。
structが「データ」を、メソッドが「振る舞い」を担当する、という概念が少し理解できたような気がします。
【実践】Web APIをメソッドで構造化する
それでは、これまでの記事で作成したシンプルなAPIを、メソッドを使って構造化してみましょう。
STEP1: リファクタリング前のコード
今回は以下のような簡単なコードを、レシーバとメソッドの考え方を使ってリファクタリングします。
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
}
func main() {
http.HandleFunc("/hello", helloHandler)
log.Println("Server starting on port 8080...")
http.ListenAndServe(":8080", nil)
}
このコードはシンプルですが、ハンドラが増えてきたり、ハンドラがDB接続やロガーなどの依存を持つようになると、main関数が肥大化し、管理が難しくなります。
STEP2: サーバーを表すstructを定義する
APIサーバー全体を表現するServerというstructを定義します。
将来的には、このstructにロガーやDB接続などの、アプリケーション全体で共有したい依存関係を持たせることになりますが、今は空白でも構いません。
同時にこのstructの新しいインスタンスを作成するためのコンストラクタ関数も定義します。
package main
// ... importは省略 ...
// Server structは、APIサーバーの依存関係を保持する
type Server struct {
// logger *zap.Logger
// db *sql.DB
// のように、将来の依存をここに追加していくが今は空白でよし
}
// NewServerはServerの新しいインスタンスを作成するコンストラクタ関数
func NewServer() *Server {
return &Server{}
}
// ... main関数など ...
STEP3: ハンドラをServerのメソッドに変換する
helloHandlerを、Server構造体のメソッドに変更します。
// ... Server structの定義など ...
// helloHandlerをServerのメソッドにする
func (s *Server) helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello from a method!")
}
// ... main関数など ...
レシーバを (s *Server) のようにポインタにしている点に注目してください。
メソッド内でstructのフィールドを変更する可能性がある場合(例えば、カウンターをインクリメントするなど)、ポインタレシーバを使うのが一般的です。
ポインタの概念については今はここで解説はしませんが、ポインタにするかしないかで、メソッド内からでも与えられたレシーバの中身を変更できるかどうかが変わる、くらいに考えてもらったら大丈夫かと思います。
STEP4: main関数でメソッドを登録する
最後に、main関数でServerのインスタンスを生成し、そのメソッドをHTTPハンドラとして登録します。
最終的に以下のようになります。
package main
import (
"fmt"
"log"
"net/http"
)
type Server struct{}
func NewServer() *Server {
return &Server{}
}
func (s *Server) helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello from a method!")
}
func main() {
// Serverのインスタンスを作成
server := NewServer()
// serverインスタンスのhelloHandlerメソッドをハンドラとして登録
http.HandleFunc("/hello", server.helloHandler)
log.Println("Server starting on port 8080...")
http.ListenAndServe(":8080", nil)
}
【実践】interfaceで疎結合にする
メソッド化によってコードは整理されましたが、インターフェースと組み合わせることでより効果を発揮します。
インターフェースは、コードを疎結合にすることができ、テストが容易になります。
例として、先ほどの完成型のコードの挨拶のメッセージを生成する部分を、別のstructに切り出し、それをインターフェース経由で利用するように変更してみましょう。
STEP1: 「振る舞い」をインターフェースとして定義する
まず、「挨拶をする」という振る舞い、すなわちメソッドを持つことをGreeterインターフェースとして定義します。
interfaceにおいては、「振る舞いの中身を定義せず、振る舞いを定義する」 という点が大切です。
// GreeterインターフェースはGreetメソッドを持つことを要求する
type Greeter interface {
Greet() string
}
STEP2: インターフェースを実装した具体的なstructを作る
次に、このGreeterインターフェースを実装する具体的なstructを作ります。
インターフェースが要求するメソッドをすべて実装しましょう。
今回の場合だとSimpleGreeterをレシーバとしているGreet()メソッドがそれにあたります。
// SimpleGreeterはGreeterインターフェースを実装する
type SimpleGreeter struct {
Message string
}
// Greetメソッドを実装することで、SimpleGreeterはGreeterインターフェースを満たす
func (g SimpleGreeter) Greet() string {
return g.Message
}
STEP3: Serverをインターフェースに依存させる
Serverが具体的なSimpleGreeterではなく、抽象的なGreeterインターフェースに依存するように変更します。
これは依存性注入(Dependency Injection) と呼ばれる設計パターンです。
今まではstringなどの具体的かつ単純な型でしか返せなかったものが、振る舞いを定義できるようになったため、メソッドを返すことができる、という点で、より幅広い返り値の定義が可能になります。
type Server struct {
greeter Greeter // 具体的な型ではなく、インターフェースに依存!
}
func NewServer(greeter Greeter) *Server {
return &Server{greeter: greeter}
}
func (s *Server) helloHandler(w http.ResponseWriter, r *http.Request) {
// インターフェースのメソッドを呼び出す
message := s.greeter.Greet()
fmt.Fprint(w, message)
}
STEP4: main関数で依存を注入する
最後に、main関数で具体的なSimpleGreeterのインスタンスを生成し、NewServerに渡してあげます。
これでなんとSimpleGreeterという構造体を生成するだけで、カプセル化して処理を書くことができました。
わかりやすいですね。
func main() {
// 具体的な実装を作成
greeter := &SimpleGreeter{Message: "Hello, Dependency Injection!"}
// 依存(greeter)をサーバーに注入
server := NewServer(greeter)
http.HandleFunc("/hello", server.helloHandler)
log.Println("Server starting on port 8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
interfaceを使うことの利点
helloHandlerは、挨拶メッセージがどこから来るのか(SimpleGreeterからなのか、DBからなのか、はたまたテスト用のダミーからなのか)という型を一切知る必要がなくなりました。
GreeterインターフェースのGreet()メソッドを呼ぶことしか知りません。
これにより、テストがとても簡単になります。
helloHandlerをテストする際に、SimpleGreeterの代わりに、テスト用のMockGreeterを作ってNewServerに渡すだけで、外部依存なしにハンドラのロジックだけをテストできます。
// main_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
// テスト用のモック(Greeterインターフェースを実装)
type MockGreeter struct{}
// Greetメソッドを実装
func (m MockGreeter) Greet() string {
return "Hello from Mock!"
}
// ServerのhelloHandlerをテストする関数
func TestHelloHandler(t *testing.T) {
// 1. テスト用のモックを準備
mockGreeter := MockGreeter{}
// 2. モックを注入してサーバーのインスタンスを作成
server := NewServer(mockGreeter)
// 3. テスト用のHTTPリクエストを作成
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
// 4. レスポンスを記録するためのRecorderを作成
rr := httptest.NewRecorder()
// 5. ハンドラー関数を直接呼び出し、リクエストとレコーダーを渡す
server.helloHandler(rr, req)
// 6. レスポンスのステータスコードを検証
if status := rr.Code; status != http.StatusOK {
t.Errorf("ステータスコードに問題があります: got %v want %v",
status, http.StatusOK)
}
// 7. レスポンスのボディを検証
expected := "Hello from Mock!"
if rr.Body.String() != expected {
t.Errorf("テスト結果が違います: got %v want %v",
rr.Body.String(), expected)
}
}
おわりに
この記事では、GoのAPIをより構造化し、疎結合にするためのレシーバとメソッド、インターフェースの使い方を解説しました。
関数をベタ書きする段階から一歩進めたので、成長した気がします。
今回学んだ設計も、あらゆる場面で応用できると思うので、handlerを使う場面に限らず、さまざまな場面において、この考え方が応用できたらいいなと感じました。
最後までお読みいただき、ありがとうございました!
この「学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025」では、GoやJavaを使い、APIの作り方、データベースとの接続、テストやDockerといった気になったバックエンド技術の基本を振り返った学びを共有しています。
ぜひ他の記事もチェックして、筆者がこのひとりアドカレを完遂することができるか、確認してみてください(^^)
学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025
それでは、明日の「学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025」の記事もお楽しみに!