📚

GoのNamed return valueについてメリデメを考える

2022/02/20に公開

はじめに

最近、GoのNamed return valueに関する話題を見かけたり、コードレビューでNamed return valueについて説明をすることがありました。
せっかくなので、Named return valueのメリデメを自分の主観でまとめます。

Named return value

Goでは、関数の戻り値に名前をつけて変数として利用することができます。
この仕組みをNamed return valueといいます。

メリット

変数名から、戻り値が何かわかる

関数の戻り値に名前をつけると、変数名から何の値かわかるようになります。
関数名や型から戻り値が何か明らかでないときに役立ちます。

具体例を示すために、Effective Goのサンプルコードを引用します。

nextInt関数はint型の変数を2つ返します。
しかし、nextIntという名前から何の値が返されるかはわかりません。
戻り値を知るために関数の中身を読む必要があります。

func nextInt(b []byte, pos int) (int, int) {

戻り値に名前をつけると、変数名から何の値が返されるかわかります。
戻り値を知るために関数の中身を読む必要はないです。

func nextInt(b []byte, pos int) (value, nextPos int) {

コードが短くなることがある

Named return valueを使うと、関数を実行し始めるときに変数が宣言されます。
関数の中で変数を宣言する必要がないので、関数内のコードが短くなります。

具体例を示すために、Effective Goのサンプルコードを引用します。

以下の関数では、引数のbufから読み取った値を、nに足しています。
Named return valueを使うと、関数内で変数nを宣言する必要がありません。

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

シンプルに実装できることがある

Named return valueを使うとシンプルに実装できることがあります。
例えば、関数内のdefer funcで戻り値に値をセットするパターンです。

func f() (err error) {    
    defer func() {
        err = errors.New("error in defer func")
    }()
    
    return errors.New("error in f")
}

Named return valueを使わない場合、関数fをwrapする関数を用意するなど、実装が複雑になる可能性があります。

メモリ効率が良い & CPUサイクルが節約できることがある

minioというGo製のオブジェクトストレージがあります。
その開発者が2017年に書いたブログを引用します。
https://postd.cc/golang-internals-part-2-nice-benefits-of-named-return-values/

あるコードでは、Named return valueを使うことで、使わない場合と比べて、メモリ効率が良い & CPUサイクルが節約できる、と述べられています。
そして、minioでは名前付き戻り値がますます採用されていくことになる、と書かれています。

以上の主張が正しければ、オブジェクトストレージのように、ハイパフォーマンスな処理性能を求められるアプリケーションではNamed return valueを使うほうがよいと思います。

デメリット

変数のスコープが広い

Named return valueを使うと、関数を実行し始めるときに戻り値の変数が宣言されます。
そのため、必要以上に変数のスコープが広くなり、コードの可読性が低下する恐れがあります。

Goのコーディングスタイルガイド

どのようなコードを書くかは、コーディングスタイルガイドを参考にして決めるのが良いと思います。
以下の、Goの代表的なコーディングスタイルガイドを確認しました。

Effective Go

Effective Goには以下のように書いてあります。

The names are not mandatory but they can make code shorter and clearer

Named return valueを使うとコードをより短く、明確にします。

Code Review Comments

Code Review Commentsでは、戻り値が何かわからないときや、遅延クロージャーで戻り値を変更するときにNamed return valueを使う。
それ以外は、繰り返しになるのでNamed return valueを使わないほうがいい、と書かれています。

繰り返しになるので

わかりづらいので補足します。
Code Review Commentsでは、サンプルコードとして以下が例示されています。

func (n *Node) Parent1() (node *Node) {}
func (n *Node) Parent2() (node *Node, err error) {}

変数nodeは、Node型のポインタです。
型から何の変数かわかるので、繰り返しになる(repetitive)と表現していると思いました。

The Uber Style Guide Go

The Uber Style Guide Goには、Named return valueについて書かれていません。
ですが、変数のスコープは小さくする(Reduce Scope of Variables)ことに言及しています。

私の判断基準

コーディングスタイルガイドの内、私は、Code Review CommentsとThe Uber Style Guide Goの意見を参考にしています。

私の主観ですが、Goはシンプルに書ける代わりに、コード量が多くなりがちな言語だと思います。
「コードをより短く」するより、視認性の向上や、変数のスコープを狭くするほうが、コードの可読性という観点でメリットが大きいと思っています。

以上のメリデメを参考にして、私が、Named return valueをつかうかどうかを判断するときの基準を書いてみました。
私なら、以下の項目を上から順番に確認します。

  1. Named return valueを使うほうがシンプルに実装できるか
  2. 関数名や型から戻り値が何かわかるか
  3. ハイパフォーマンスな処理性能を求められているか
  4. 1-3にあてはまらないなら、Named return valueを使わない

1. Named return valueを使うほうがシンプルに実装できるか

メリットに書いたとおりです。

2. 関数名や型から戻り値が何かわかるか

関数名から型から戻り値が何かわからない場合、コードの可読性の観点からNamed return valueを使います。

ただ、関数名や型から戻り値が何であるかわからないときは、関数名や型の設計を間違えていることがあります。
そのため、Named return valueを使う前に、関数名や型の設計を見直すほうがいいです。

例えば、以下のような関数があるとします。
GetUserInfoは、idを引数にとって、ユーザーの名前と年齢、エラーを返す関数です。
関数名と戻り値の型から、戻り値が何であるかはわかりません。

func GetUserInfo(id string) (string, int, error) {
...
}

まず、関数名と戻り値の型を見直します。
関数名を愚直に変更するなら、GetUserNameAndAgeと書きます。
関数名からユーザーの名前と年齢を取得するとわかります。

func GetUserNameAndAge(id string) (string, int, error) {
...
}

GetUserInfoという名前からユーザー情報を取得する関数かもしれません。
それなら、ユーザー情報を詰め込む構造体をつくるほうが良いと思います。

例えば以下のように書けます。
戻り値の型から、関数が何を返すかわかりやすくなりました。

type User struct {
    Name string
    Age  int
}

func GetUser(id string) (*User, error) {
...
}

3. ハイパフォーマンスな処理性能を求められているか

関数がハイパフォーマンスな処理性能を求められている場合、Named return valueを使うメリットはあります。

このとき、始めからNamed return valueを使って実装するのは避けたほうがいいと思います。
Named return valueを使わない実装で処理性能を計測しないと、処理性能を改善すべき関数かどうかわかりません。

4. 1-3にあてはまらないなら、Named return valueを使わない

1-3にあてはまらないなら、私はNamed return valueを使いません。

Named return valueを使うことで処理性能は向上するかもしれませんが、戻り値の変数のスコープが広くなります。
3にあてはまらないので、処理性能の向上によるメリットは小さいです。
そのため、戻り値の変数のスコープが広くなることによる可読性低下のデメリットのほうが大きいので、Named return valueを使わないほうがいいと思っています。

まとめ

自分の主観にもとづいてNamed return valueのメリデメを考えました。

繰り返しになりますが、私はGoを使うときに、必要以上に処理性能を向上させることよりも、変数のスコープを狭くして可読性を向上させることを好んでいます。
そのため、基本的にNamed return valueは使わず、Named return valueを使うことでメリットがあるときのみ使う、方針にしました。

人によっては常にNamed return valueを使うべきだ、という人もいると思います。
また、チームでコーディングスタイルガイドを決めていれば、それに従うのが良いと思います。
あくまで、私個人の意見として読んでいただけると嬉しいです。

本文では、Named return valueを使うとメモリ効率が良い & CPUサイクルが節約できることについて、引用した記事の内容が正しいと仮定して議論をしました。
2017年の記事なので、最新版のGoであれば結果が異なる可能性があることに注意してください。

本文に書いていないNamed return valueのメリデメがあれば、ぜひ教えてください。

Discussion