はじめに
こんにちは。Nubです。
研究や個人開発でオレオレ開発を行っていた私ですが、遂にコードレビューをしていただくことになりました。年貢の納め時です。
今回は、Go言語での変数やプログラムのスコープについて絞って書いていこうと思います。
要約
- 変数はlowerCamelCaseで宣言しましょう
- 変数はなるべく使う直前で宣言しましょう
- スコープ内ですべてを完結させるのは、逆に可読性を落とす可能性があるので気をつけましょう
変数の命名方法
プログラム内で変数や定数を宣言する上で、命名方法には様々な種類があります。
-
UpperCamelCase
- 各単語の先頭文字を大文字にして、単語間の空白を消してつなげる
-
lowerCamelCase
- 先頭単語が小文字、それ以外の頭文字を大文字でつなげる
-
UPPER_SNAKE_CASE
- 全アルファベットを大文字にして、空白を
_
に置き換えて繋げる
- 全アルファベットを大文字にして、空白を
-
lower_snake_case
- 全アルファベットを小文字にして、空白を
_
に置き換えて繋げる
- 全アルファベットを小文字にして、空白を
-
UPPER-KEBAB-CASE
- 全アルファベットを大文字にして、空白を
-
に置き換えて繋げる
- 全アルファベットを大文字にして、空白を
-
lower-kebab-case
- 全アルファベットを小文字にして、空白を
-
に置き換えて繋げる
- 全アルファベットを小文字にして、空白を
Go言語では、変数はlowerCamelCase
を使います。
パッケージレベルで公開する(外部から参照できる)関数や構造体メソッド等はUpperCamelCase
を使います。
定数はUPPER_SNAKE_CASE
だったり様々ですが・・・
自分個人の開発ではPythonをメインで使っているため、PEP-8で推奨されているlower_snake_case
を使うことが多く、うっかりGo言語で同じように書いて指摘されてしまいました。気をつけます。
変数は必要なタイミングで宣言する
当たり前ですが、Go言語の変数は基本どこでも宣言できます。
例えば、global
はmain関数の外で宣言されていますし、s
やlocal
はmain関数の中で宣言されています。
更に、s
やlocal
は関数の中でfmt.Println
が使われる前後で宣言されていても特に問題はありません。
つまり、変数は前後に何かしら処理が入っていても問題なく宣言できるということです。
package main
import "fmt"
var global = "グローバル変数"
func main() {
fmt.Println(global) // グローバル変数
var s string
fmt.Println(s) // (空文字)
s = "ローカル変数(varによる宣言)"
fmt.Println(s) // ローカル変数(varによる宣言)
local := "ローカル変数(省略変数宣言)"
fmt.Println(local) // ローカル変数(省略変数宣言)
}
この書き方も可能で、なんとなく収まりが良さそうに見えます。
func main() {
var s string
local := "ローカル変数(省略変数宣言)"
fmt.Println(global) // グローバル変数
fmt.Println(s) // (空文字)
s = "ローカル変数(varによる宣言)"
fmt.Println(s) // ローカル変数(varによる宣言)
fmt.Println(local) // ローカル変数(省略変数宣言)
}
ですが、local
が最後の行のみで使われていることから、わざわざ最初に宣言する必要がありません。
そのため、変数は使う直前で宣言する方が、別の人が読むときに 「この変数っていつ使うの?」という気持ちを持たせることを防ぐ ことができます。
func something() {
/*
variable が影響しない処理を書く
*/
variable := "何かしらの初期化処理"
/*
variable が影響する処理を書く
*/
}
スコープを意識して変数を扱う
先程のコードに続き、for文やif文などが入った場合はどうでしょうか。
package main
import (
"fmt"
"strconv"
)
func NumberStringToIntArray(s string) ([]int, error) {
var intArray []int
for _, r := range s {
num, err := strconv.Atoi(string(r))
if err != nil {
return []int{}, err
}
intArray = append(intArray, num)
}
return intArray, nil
}
func main() {
fmt.Println(NumberStringToIntArray("12345")) // [1 2 3 4 5] <nil>
fmt.Println(NumberStringToIntArray("12-45")) // [] strconv.Atoi: parsing "-": invalid syntax
}
NumberStringToIntArray
は数字列をintのスライス配列に直して返します。
この時、intArray
は関数全体を影響範囲に持ち、for文の中でも参照されています。
num
,err
はfor文の中で定義されているため、for文の外では参照できない変数になります。
ここで、if文は変数の宣言が出来るよな?と思う方もいるかも知れません。その場合はこのような書き方になります。
func NumberStringToIntArray(s string) ([]int, error) {
var intArray []int
for _, r := range s {
// num, err := strconv.Atoi(string(r)) を if文の中に入れる
if num, err := strconv.Atoi(string(r)); err != nil {
return []int{}, err
} else {
intArray = append(intArray, num)
}
}
return intArray, nil
}
num
,err
はfor文の中のif文で定義されているため、if文の外では参照できない変数になります。
一時変数はなるべく小さいスコープ内に留める狂信者の自分は、この書き方でいいやん!と思い書いていたのですが、これは避けるべき書き方です。
Goの慣習として、「ifブロック内でエラーを処理してリターンし、成功ケースはインデントしない」というものがあります。
つまり、エラー処理が走らない場合はインデントを戻せ = エラーハンドリングでelseを使うなということになるので、過度なスコープ意識は避けるべきだと思います。
func something() error {
/*
正常な動作をしていた
*/
// 何かしらの処理をして、エラーが返るかチェック
variable, err := ReturnSomethingValueAndError()
if err != nil {
// エラーの場合はここで終了して返す
return err
}
/*
問題ないなら、正常な動作を続ける
*/
}
まとめ
コードをレビューされる機会をいただいて、以下のような指摘を受けました。
-
命名をミスしないようにしましょう
- 変数は
lowerCamelCase
- 関数やメソッドは
UpperCamelCase
- 変数は
-
変数は必要なタイミングが来たときに宣言しましょう
- 変数の影響範囲を小さくする
- 「これっていつ使うの?」を思わせない
-
スコープの取り扱いはバランスよくしましょう
- 正常系のコードはインデントが最も浅い
- エラーされたらリターンして終わる(elseブロックを作らない)
これを忘れずに、バランスの良いコードを書けるように今後も精進していきます。
余談
自分が本格的にプログラムを書き始めたのは大学1年の春、64bitプロセッサを積んだPCでBorland C++コンパイラを使いC言語をコンパイルして、32bit用バイナリを吐き続けていました。
その時の参考書は軒並み変数は関数の最初に全て書く形式となっており、いわゆる古代C言語の記法になっていました。(簡単のためもありますが)
その後、チームでの開発経験や他人のコードを読み解いて思想を合わせるという経験が少なく、そのまま古代C言語の書き方に近い書き方を行っていました。
更に、C++,Java,Python等の言語に触れ、書き方が奇妙なことになってしまったので今後矯正していこうと思います。