34
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

HRBrainAdvent Calendar 2023

Day 2

変数宣言とスコープの取り扱いに気をつけよう

Last updated at Posted at 2023-12-01

はじめに

こんにちは。Nubです。

研究や個人開発でオレオレ開発を行っていた私ですが、遂にコードレビューをしていただくことになりました。年貢の納め時です。

今回は、Go言語での変数やプログラムのスコープについて絞って書いていこうと思います。

要約

  • 変数はlowerCamelCaseで宣言しましょう
  • 変数はなるべく使う直前で宣言しましょう
  • スコープ内ですべてを完結させるのは、逆に可読性を落とす可能性があるので気をつけましょう

変数の命名方法

名称未設定のデザイン.png
問 : これな〜んだ?
解 : 命名規則

プログラム内で変数や定数を宣言する上で、命名方法には様々な種類があります。

  • 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関数の外で宣言されていますし、slocalはmain関数の中で宣言されています。
更に、slocalは関数の中で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
    }

    /*
       問題ないなら、正常な動作を続ける
    */
}

まとめ

コードをレビューされる機会をいただいて、以下のような指摘を受けました。

  1. 命名をミスしないようにしましょう
    • 変数はlowerCamelCase
    • 関数やメソッドはUpperCamelCase
  2. 変数は必要なタイミングが来たときに宣言しましょう
    • 変数の影響範囲を小さくする
    • 「これっていつ使うの?」を思わせない
  3. スコープの取り扱いはバランスよくしましょう
    • 正常系のコードはインデントが最も浅い
    • エラーされたらリターンして終わる(elseブロックを作らない)

これを忘れずに、バランスの良いコードを書けるように今後も精進していきます。

余談

自分が本格的にプログラムを書き始めたのは大学1年の春、64bitプロセッサを積んだPCでBorland C++コンパイラを使いC言語をコンパイルして、32bit用バイナリを吐き続けていました。

その時の参考書は軒並み変数は関数の最初に全て書く形式となっており、いわゆる古代C言語の記法になっていました。(簡単のためもありますが)

その後、チームでの開発経験や他人のコードを読み解いて思想を合わせるという経験が少なく、そのまま古代C言語の書き方に近い書き方を行っていました。
更に、C++,Java,Python等の言語に触れ、書き方が奇妙なことになってしまったので今後矯正していこうと思います。

参考

34
8
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
34
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?