Goとの出会い

自分がGoを触り始めたきっかけは、大学院1回生の時に研究室の先輩と一緒に出たISUCONでした。パフォーマンスの観点でGoがめっちゃいいらしいからGoで出ようってなって、Go Tourなんかで勉強してました。

当時なんでもGithubにpushしたい病だったので、よーわからんリポジトリもありました。

https://github.com/adshidtadka/go_tour

エラーハンドリングがめんどくさい

そもそもプログラミングをちゃんと始めたのが大学4回生のDonutsでのアルバイトで、経験が浅いなんちゃてプログラマーでした。つまり、エラーハンドリングなんてかったるいと思っているわけです。

そんなマインドでGoのコードを読むと、どんな関数もerrorを返してくることにびっくりします。

こんな感じで何か関数を呼び出すとすかさずerrorが帰ってくるので、いちいちエラーハンドリングを書くのがめんどくさく握りつぶしたいという思いに駆られることが幾度となくありました。

type Result struct{}

func doSomething(code string) (*Result, error) {
	if code == "ok" {
		return &Result{}, nil
	}
	return nil, errors.New("エラーだよ")
}

func main() {
	_, err := doSomething("error")
	if err != nil {
		// エラーハンドリングをいちいち書かされる
	}
}

他の言語の多くにはtry-catchの仕組みがあるので、こっちのが絶対便利とか思っていました。


type Result = {};

const doSomething = (code: string): Result => {
  if (code === "ok") {
    return {};
  }
  throw new Error("エラーだよ");
};

const main = () => {
  try {
    const result = doSomething();
  } catch (e) {
    // ここ1箇所でまとめてエラーハンドリングできる
  }
};

やっぱりエラーハンドリングしたい

大学3年、社会人2年の時を経て考え方が変わりました。関数たちに対して思うことはエラーを勝手にthrowせずにちゃんと返して欲しいと思うようになりました。

その理由の一例がこちらです。最近プロダクトを開発する中で、不具合調査に時間がかかった例でもあります。

type Result = {};

const doSomething = (code: string): Result => {
  if (code === "ok") {
    return {};
  }
  throw new Error("エラーだよ");
};

const main = () => {
  try {
    const result = doSomething();
    const result2 = doSomething(); // こいつが呼ばれるかどうかわからない
  } catch (e) {

  }
};

try句のコードは全て呼ばれるわけではないのです。勝手にジャンプしてcatch句にいく可能性を考えてコーディングする必要があります。try句には長い処理を書かない方が無難でしょう。

一方Goで書いたとすると


type Result struct{}

func doSomething(code string) (*Result, error) {
	if code == "ok" {
		return &Result{}, nil
	}
	return nil, errors.New("エラーだよ")
}

func main() {
	_, err := doSomething("error")
	if err != nil {
		return
	}
	_, err = doSomething("error") // よばれないかもしれないことがわかりやすい
	if err != nil {
		return
	}
}

となります。エラーが起きたらこうするというのが各所でちゃんと書かれるので、実行するときの安心感が格別なわけです。

関数がネストしてたら大変…?

関数の呼び出しがネストするとエラーハンドリングはどうなるでしょうか。

try-catchが書けると、特に気にせずcatchの処理に飛んでくれます。


type Result = {
  data: string;
};

const doSomethingInSomething = (code: string): Result => {
  if (code === "ok") {
    return {
      data: "ok",
    };
  }
  throw new Error("doSomethingInSomethingでエラーだよ");
};

const doSomething = (code: string): Result => {
  if (doSomethingInSomething(code).data === "ok") {
    return {};
  }
  throw new Error("doSomethingでエラーだよ");
};

const main = () => {
  try {
    const result = doSomething();
  } catch (e) {
    // ここ1箇所でまとめてエラーハンドリングできる
  }
};

しかし、Goの場合はtry-catchが書けないので末端の関数で起きたエラーをうまく上位の関数に伝播させる仕組みが必要です。

エラー定義とハンドラを作ればいい感じに伝播させられる

こんな感じで書けば解決します。


package main

import (
	"github.com/pkg/errors"
)

type ErrDoSomething struct {
	error
}

func NewErrDoSomething(msg string) error {
	return &ErrDoSomething{
		errors.New(msg),
	}
}

type ErrDoSomethingInSomething struct {
	error
}

func NewErrDoSomethingInSomething(msg string) error {
	return &ErrDoSomethingInSomething{
		errors.New(msg),
	}
}

func ResolveError(err error) {
	switch errors.Cause(err).(type) {
	case ErrDoSomething:
		// エラーハンドリング
	case ErrDoSomethingInSomething:
		// エラーハンドリング
	}
}

type Result struct {
	data string
}

func doSomethingInSomething(code string) (*Result, error) {
	if code == "ok" {
		return &Result{
			data: "ok",
		}, nil
	}
	return nil, NewErrDoSomethingInSomething("doSomethingInSomethingでエラーだよ")
}

func doSomething(code string) (*Result, error) {
	result, err := doSomethingInSomething(code)
	if err != nil {
		return nil, errors.Wrap(err, "doSomethingでエラーだよ")
	}
	if result.data == "ok" {
		return &Result{}, nil
	}
	return nil, NewErrDoSomething("doSomethingでエラーだよ")
}

func main() {
	_, err := doSomething("error")
	ResolveError(err)
}

errors.Wrap(err, "doSomethingでエラーだよ") みたいな感じで情報をつけくわえておくと、ログではいた時にどうさの調査がしやすいです。

まとめ

Goが好まれる理由の1つとして、こういったエラーハンドリングの記述を強制させることで、誰が書いても読みやすいコードになるところなのかなと思います。