41
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goに入門して、ついでにクリーンアーキテクチャに入門した ーその1

Last updated at Posted at 2025-03-28

はじめに

業務でLaravelを使ってMVCでアプリケーションを作成したことはありますが、どうやらGoは「Clean Architecture(クリーンアーキテクチャ)」で開発するのが主流なようです。

今回はコードを書いたりAIに聞いたりしながらGoとクリーンアーキテクチャについて学んでみました!

思考整理のためのアウトプットですので、情報の正確性などは保証できません!ご容赦ください:cop_tone2:

インターフェース指向

クリーンアーキテクチャで重要になってくるのがインターフェースです。

ChatGPTによると、Goは「インターフェース指向」の言語と言われているようです。
オブジェクト指向はよく耳にしますが、インターフェース指向という言葉もあるんですね。

インターフェースという概念はどの言語にも存在すると思いますが、GoのインターフェースはPHPなどのそれとは多少異なるらしく、わかりやすくChatGPTに教えてもらいました。

例えばJavaやC#では、あるクラスがインターフェースを「implements」や「: InterfaceName」として明示的に実装する必要があります。
でもGoでは…
対応するメソッドを「たまたま」持っていれば、そのインターフェースを満たしているとGoは判断します。

うーん、よくわかりません笑

ということで、いったんmain.goにインターフェースを含むコードを書いてみて、インターフェースについてわかってきたらクリーンアーキテクチャについても深掘りしていこうと思います。

Goのインターフェースを理解する

例としてAnimalインターフェースを定義してみます。
最初にコード全体を貼っておきます。

main.go
package main

import (
	"fmt"
)

// インターフェースの定義
type Animal interface {
	Speak() string
}

type Dog struct {
	Name string
}

// Dog に Speak() メソッドを実装(インターフェースを満たす)
func (d Dog) Speak() string {
	return d.Name + " says: わん!"
}

type Cat struct {
	Name string
}

func (c Cat) Speak() string {
	return c.Name + " says: にゃー!"
}

// Animal インターフェースを受け取る関数
func MakeItSpeak(a Animal) {
	fmt.Println(a.Speak())
}

func main() {
	dog := Dog{Name: "ポチ"}
	cat := Cat{Name: "ミケ"}

	MakeItSpeak(dog)
	MakeItSpeak(cat)
}

ターミナルでgo run main.goを実行することで、ポチsays:わん! ミケsays:にゃー!が出力されます。

では順番にコードを追っていきましょう。


type Animal interface {
	Speak() string
}

type Dog struct {
	Name string
}

type Cat struct {
	Name string
}

Animalインターフェースでは「Speak()」という、戻り値の型が文字列のメソッドを定義しています。

また、 Dog ・ Cat それぞれの構造体が定義されています。


func (d Dog) Speak() string {
	return d.Name + " says: わん!"
}

func (c Cat) Speak() string {
	return c.Name + " says: にゃー!"
}

次に Dog・Cat 構造体をレシーバーとして、自分の鳴き声を返すSpeak()メソッドを実装します。

Goでは、構造体がSpeak()メソッドを持っていれば、それだけでAnimalインターフェースを満たしている、と暗黙的に判断されます。

この実装は構造体とインターフェースの間に明示的な結びつきを持たないため、疎結合な設計を自然に実現できるようです。

レシーバーについて補足 「値レシーバー」と「ポインタレシーバー」

仮にメソッド内の処理として、構造体のフィールド(他の言語でいうクラスのメンバー・プロパティ)の値を更新したい場合

func (*d Dog) Speak() string {}

のように構造体の変数の前にアスタリスクを記述します。

こうすると、メソッド内でフィールドを変更すると参照元の構造体にもその変更が反映されます!

アスタリスクが付く方はポインタレシーバー、付かない方は値レシーバーと言います。


func MakeItSpeak(a Animal) {
	fmt.Println(a.Speak())
}

そして、MakeItSpeak()関数は引数にAnimal型をとっており、DogやCatはSpeak()メソッドを実装しているため、Animalインターフェースを満たしているとみなされます。

※この後の章で触れますが、他の言語のように明示的に「Animal型を実装します」という宣言が不要なのがGoの特徴です。(例:implementsキーワードなど)
そのため、上記の説明も満たしていると"みなされる"というニュアンスになります。
より厳密にいうとGoのコンパイラが自動で判断する、ということです。

そのため、Animal型の引数をとるMakeitSpeak()関数に渡すことができるということです。


func main() {
	dog := Dog{Name: "ポチ"}
	cat := Cat{Name: "ミケ"}

	MakeItSpeak(dog)
	MakeItSpeak(cat)
}

ということで、最終的にmain関数内でMakeItSpeak()に Dog・Cat(=Animalインターフェース)を渡して実行することで、それぞれの動物の鳴き声が出力されました!

PHPでAnimalインターフェースを書いてみる

試しにPHP(オブジェクト指向言語)でAnimalインターフェースがどのようなコードになるか比較すると、よりGo言語の特徴がわかりやすいかと思ったので、AIにお願いして書いてもらいました。

PHP
<?php
interface Animal {
    public function speak(): string;
}

class Dog implements Animal {
    private string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function speak(): string {
        return $this->name . " says: わん!";
    }
}

class Cat implements Animal {
    private string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function speak(): string {
        return $this->name . " says: にゃー!";
    }
}

function makeItSpeak(Animal $animal) {
    echo $animal->speak() . PHP_EOL;
}

$dog = new Dog("ポチ");
$cat = new Cat("ミケ");

makeItSpeak($dog);
makeItSpeak($cat);

こうして見比べてみると、割と異なる点が多いように見えます。

PHPではimplementsというキーワードを使ってインターフェースを明示的に実装する必要があります。

PHP
class Dog implements Animal {...}
class Cat implements Animal {...}

また、Goにはコンストラクタ関数が用意されていないため、dog := Dog{Name: "ポチ"}のように直接初期化する必要がありますが、$this変数を用いて__constructor関数で初期化しています。

PHP
public function __construct(string $name) {
    $this->name = $name;
}

また、メソッドについてですが
Go ・・・・構造体が持つ機能・振る舞い
PHP ・・・クラスが持つ機能・振る舞い

このような捉え方をすると同じもののように思えますが、実際には少し意味合いが異なるようです。

Go
type Dog struct {
	Name string
}

func (d Dog) Speak() string {
	return d.Name + " says: わん!"
}

Dogという構造体にSpeak()という振る舞いを「結びつけている」

PHP
class Dog {
	private string $name;

	public function __construct(string $name) {
		$this->name = $name;
	}

	public function speak(): string {
		return $this->name . " says: わん!";
	}
}

Dogというクラスにspeak()という振る舞いを「定義している」

仮に犬・猫に続いて「猿」を追加するケースを考えてみます。

猿は鳴きますし、噛み付くし、木に登ります。
この特徴の中で、鳴く・噛み付くは犬にも猫にも当てはまりますよね。

その場合、必要に応じてそれらの振る舞い=メソッドを Dog・Cat 構造体に結びつけるイメージでしょうか。
クラスにメソッドを定義するのとはちょっと感覚が違いますよね。

Go
type Speaker interface {
	Speak() string // 鳴く
}

type Biter interface {
	Bite() string // 噛み付く
}

type Climber interface {
	Climb() string // 木に登る
}

Go
type Monkey struct {
	Name string
}

func (m Monkey) Speak() string {
	return m.Name + " says: ウキキー!"
}

func (m Monkey) Bite() string {
	return m.Name + " が噛みついた!"
}

func (m Monkey) Climb() string {
	return m.Name + " が木に登った!"
}

Go
type Dog struct {
	Name string
}

func (d Dog) Speak() string {
	return d.Name + " says: わん!"
}

func (d Dog) Bite() string {
	return d.Name + " が噛みついた!"
}

Goのインターフェースの理解を深める

また、異なる例でPHPとGoのインターフェースの比較ができるコードを出してもらいました。

以下のPHPのコードは「渡された動物が Speaker や Climber を実装してるかチェックして、できる行動だけ実行する」という判定をしています。

PHP
function act($animal) {
    if ($animal instanceof Speaker) {
        echo $animal->speak();
    }
    if ($animal instanceof Climber) {
        echo $animal->climb();
    }
}

ポイントは、instanceof Speaker を使うには、そのクラスがあらかじめ implements Speaker を書いておかないとダメという点です。

PHP
interface Speaker {
    public function speak(): string;
}

class Dog implements Speaker {
    public function speak(): string {
        return "わん!";
    }
}

一方、Goはインターフェースを実装する際の宣言が不要です。
以下のコードで Dog に「Speaker を実装する」とは一言も書いていませんが、Act() に Dog を渡すと、Dog は Speak() メソッドを持っているので Speaker インターフェースを満たしている(とGoが判断する)。
よって、a.(Speaker) の型アサーションも true になり、問題なく動作します。

Go
type Speaker interface {
	Speak() string
}

type Dog struct {
	Name string
}

func (d Dog) Speak() string {
	return "ワンワン!"
}

func Act(a interface{}) {
	if s, ok := a.(Speaker); ok {
		fmt.Println(s.Speak())
	}
	if c, ok := a.(Climber); ok {
		fmt.Println(c.Climb())
	}
}

つまり、型アサーションで引数の値を調べるときに、インターフェースを実装しているか知る必要はないということになります。

確かにこれは「疎結合」な設計だな、ということがわかりました。

おわりに

だいぶ内容がGoのインターフェースに関する話に偏ってしまいました。

ですがGoでクリーンアーキテクチャを使って開発するならインターフェースは重要な要素になるので、少しボリュームを割いて理解に努めました。

これで大体インターフェースについてはわかったので、次の記事ではmain.goのコードを責務ごとにファイル分割して、クリーンアーキテクチャをやっていこうと思います:taco:

参考

41
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?