♠️

TypeScriptのエンジニアがGoを使ってDDDっぽい構成でAPIを構築してみて戸惑った点

2021/10/26に公開

Goを書いてみようと思った理由

  • 単純に、普段業務で触っていない新しい言語を触ってみたい(正直、これが一番)
  • 直接実行ファイルにコンパイルするので処理速度が速い(実際に試してはいないです)
  • GinなどのWebフレームワークやORMのライブラリが整備されていて、開発しやすそう
  • パッケージ管理もしやすくなってそう
  • 言語仕様がシンプルなので覚えることがそこまで多くはなさそう

ディレクトリ構成

作ったもののディレクトリ構成はこちらです。
タイトルにDDDっぽいと書いている通り、テーブル定義のモデルをそのままドメインのモデルとして使っていたりして、実際に業務で使っていくにはまだ不十分な作りになっています。

.
├── domain
│   ├── model
│   │   └── todo.go
│   └── repository
│       └── todo.go
├── infra
│   └── todo.go
├── main.go
└── usecase
    └── todo.go

具体的なコード

GitHubにコードを上げています。
この記事では具体的な解説は省きますが、Dockerを使ってローカルで動かせるようにしています。

https://github.com/hisami/hello-gin

TypeScriptと違って戸惑った点

オブジェクト指向の機能を持たない

Goにはオブジェクト指向の機能がありません。そのため、クラス(メンバ変数、メソッド)の設計を行うかわりに構造体とメソッドの設計を行います。メソッドの定義もオブジェクト指向とは違い(=クラスの中で定義される関数)、任意の型に特化した関数を定義する仕組みのことを指します。
以下に構造体とメソッドの具体例を記載します。

// 構造体
type Point struct{
  X,Y int
}

// メソッド
func (p *Point) Print(){
  fmt.Printf("(%d,%d)", p.X, p.Y)
}

インターフェースの書き方に癖がある

Goにもインターフェースの仕組みはありますが、TypeScriptなどの他の言語と異なり、書き方に少し癖があります。具体的には、明示的にimplementsを記載する必要がなく、インターフェースで定義した関数を全て実装すれば終わりです。
以下に具体例を記載します。

// インターフェースの定義
type Employee interface{
  Work()
  Sleep()
}
// インターフェースを実装する構造体
type FullTimeEmployee struct{
  Height int // 身長
}

// インターフェースの実装
func (fte *FullTimeEmployee) Work(){
  // 処理内容
}

これだけだと、インターフェースの良さがわかりにくいかもしれませんが、以下のようにEmployeeインタフェースを戻り値に指定した関数でFullTimeEmployeeを返そうとすると、Workメソッドは実装されていますがSleepメソッドは実装されていないため、コンパイルエラーとなります。

func NewEmployee(height int) Employee {
  return &FullTimeEmployee{Height: height}
}

ポインタをある程度は理解している必要がある

C言語などにも登場し、学習時に躓きやすいポインタが、Goでも登場します。ポインタとは、メモリ上のアドレスと型を表す情報で、例えばint型のポインタを定義したい場合は*intのように記載します。
このポインタの知識が必要になってくるのは、主に関数の引数を定義する時です。
Goで関数の引数を定義する際には、C言語などと同様に、値渡しとポインタ渡しを指定することができ、どちらを選択するかで挙動が異なります。
例えば、以下のように関数の引数として構造体を渡した場合(=値渡しの場合)は、構造体のコピーが生成され、その構造体が関数によって処理され、元の構造体には影響を与えません。

type Point struct{
  X,Y int
}

func addOne(p Point){
  p.X = p.X + 1
  p.Y = p.Y + 1
}

p := Point{X: 1, Y: 2}
addOne(p)

p.X // 1のまま
p.Y // 2のまま

一方で、引数をポインタで渡すと、引数で渡した構造体に対して処理を行うことができます。

type Point struct{
  X,Y int
}

func addOne(p *Point){
  p.X = p.X + 1
  p.Y = p.Y + 1
}

p := Point{X: 1, Y: 2}
addOne(&p)

p.X // 2になる
p.Y // 3になる

配列の操作(mapなど)がやりにくい

JavaScript、TypeScriptには、配列を操作するための関数(map、filter、findなど)が豊富に用意されています。
一方で、言語仕様がシンプルなGoは基本的にはfor文しか持たないため(foreachに似た、for...rangeという構文はある)、mapやfilterなどの操作を行うには、ライブラリを使用する必要があります。
以下の記事で、go-funkgenというライブラリが紹介されていますが、生成される配列の型がイマイチだったり、アノテーションを使って簡易的にコードを書いて、実際に動かすコードはコードジェネレータを使って生成していたりして、個人的には、これらを使うよりはfor文で記載するかな、、という印象です。

https://qiita.com/croquette0212/items/54f0651e803511586eb0

ちなみに、Goには三項演算子もなかったりして、シンプルな言語仕様故に、他にも、あれ、これないの?と思うものが出てきそう。

型定義の柔軟性が低い

TypeScriptには、以下のようなunion型(合併型)やintersection型(交差型)があり、既に定義した型を再利用して、新たな型を定義することができますが、Goにはありません。

  • union型(合併型)
type NewType = number | string;
  • intersection型(交差型)
interface Hoge1 {
  foo: string;
  bar: number;
}
interface Hoge2 {
  foo: straing;
  baz: boolean;
}
type NewType = Hoge1 & Hoge2;

したがって、例えばunion型を使いたい場合は、以下などを参考に、自分で仕組みを作る必要があります。

https://qiita.com/sxarp/items/cd528a546d1537105b9d

所感

ここ2年間、主にJavaScript・TypeScriptしか触ってない自分からすると、「TypeScriptだと簡単に書けるのになー」と思う点が正直多かったです。
ただ、そこには単純な慣れの差も含まれていると思うので、引き続きGoを触っていって、業務でも使用できるレベルまで持っていきたいなと思います。

参考

https://qiita.com/ryokky59/items/6c2b35169fb6acafce15

https://qiita.com/croquette0212/items/54f0651e803511586eb0

https://qiita.com/sxarp/items/cd528a546d1537105b9d

Discussion