いろいろ備忘録日記

主に .NET とか Go とか Python絡みのメモを公開しています。

Goメモ-100 (省略変数宣言 := と変数スコープ でよくやってしまうミス )

概要

自分でも未だにたまにやるミスで、今まで他の人にも何度か「なんでこうなるの?」って聞かれたのでついでにメモメモ。

書籍「プログラミング言語 Go」に丁度いい例があったので、それを流用しています。

いきなりですが、以下のコードを御覧ください。

package scope

import (
    "os"

    "github.com/devlights/gomy/output"
)

var (
    // 現在の作業ディレクトリ. CommonMistake1関数で使う前に初期化されている想定
    _cwd1 string
)

func loadcwd() error {
    _cwd1, err := os.Getwd()
    if err != nil {
        return err
    }

    output.Stdoutl("[loadcwd]", _cwd1)

    return nil
}

func CommonMistake1() error {
    if err := loadcwd(); err != nil {
        return err
    }

    output.Stdoutl("[main]", _cwd1)

    return nil
}

実行すると、CommonMistake1関数が呼ばれるとして、なんて出力されると思いますか?

こうなります。

$ make run
ENTER EXAMPLE NAME: scope_common_mistake1
[Name] "scope_common_mistake1"
[loadcwd]            /home/devlights/dev/try-golang
[main]               


[Elapsed] 179.958µs

loadcwd関数で出力したときはちゃんと出てるのに、呼び出し元で確認すると空文字になっています。

当然ながら、コンパイルもちゃんと通る。

なんで?

てことで、さっきのコードにコメントで補足をいれてみました。

package scope

import (
    "os"

    "github.com/devlights/gomy/output"
)

var (
    // 現在の作業ディレクトリ. CommonMistake関数で使う前に初期化されている想定
    _cwd1 string
)

func loadcwd() error {
    // 一見、ちゃんと os.Getwd() の結果を パッケージ変数 _cwd1 に設定できているように見えるが・・
    _cwd1, err := os.Getwd()
    if err != nil {
        return err
    }

    // ここでの結果はちゃんと表示される
    output.Stdoutl("[loadcwd]", _cwd1)

    return nil
}

// CommonMistake1 -- 変数宣言のスコープによるよくやる間違いについてのサンプルです.
func CommonMistake1() error {
    if err := loadcwd(); err != nil {
        return err
    }

    output.Stdoutl("[main]", _cwd1)

    // -------------------------------------------------------------------------
    // 実行すると、上の _cwd1 の値は空文字で出力される.
    // 理由は、loadcwd() で設定している _cwd1 は、省略変数宣言 := を使っているため
    // ローカル変数 _cwd1 が新たに生成されてしまったため。
    //
    // loadcwd() 内で、最後にログ出力変わりに ローカル変数 _cwd1 の値を出力する
    // ようにしているため、一見ちゃんと設定できているように見えるし
    // ログ出力するために変数を使用しているため、コンパイルエラーにもならない
    // (これがログ出力部分がなかったら、ローカル変数を使用していないという
    //  コンパイルエラーとなるため、そこで気づける可能性がある。)
    // -------------------------------------------------------------------------

    return nil
}

Goでは、レキシカルブロック毎にスコープがあります。要は、ブロック毎にスコープが作られるということですね。

なので、関数作ったら、その関数のスコープ、関数の中で if ブロック作ったら、その if ブロックのスコープって感じとなります。

で、 Go で :=省略変数宣言 です。これ、使っているところを初見でみると型を省略した代入みたいに見えるんですが

変数宣言となっています。代入じゃない。

なので、

package main

import "fmt"

func main() {
    x := return1()
    if x == 1 { 
        x := x + 1 
        fmt.Printf("[if(1-1)] %v\n", x)

        if x == 2 { 
            x := x + 1 
            fmt.Printf("[if(2)]   %v\n", x)
        }   

        fmt.Printf("[if(1-2)] %v\n", x)
    }   

    fmt.Printf("[main]    %v\n", x)
}

func return1() int {
    return 1
}

は、実行すると

[if(1-1)] 2
[if(2)]   3
[if(1-2)] 2
[main]    1

となります。

最初の例は、これのパッケージ変数が絡んでいる版なのですが、パッケージ変数が絡むとよく間違えちゃうんですよねこれが・・。

関数のローカル変数として宣言されてしまうので、使っていなければコンパイルエラーですぐ分かるんですが、大抵ログ出力とかするので、コンパイルエラーにもならずに通って、実行すると「あれ?」ってなることが多い・・。

解決策

で、それを防ぐためのやり方ですが、いろいろあると思いますが、私は大体以下のどちらかでやってます。

省略変数宣言せずに済ます

普通ですが、シャドーイングが発生する箇所では、:= を使わずにちゃんと = で処理する。

package scope

import (
    "os"

    "github.com/devlights/gomy/output"
)

var (
    _cwd2 string
)

func loadcwd2() error {
    // ローカルスコープで _cwd2 が生成されるのを防ぐために
    // err を var宣言 で先に宣言し、:= を使わないようにする
    var err error

    _cwd2, err = os.Getwd()
    if err != nil {
        return err
    }

    output.Stdoutl("[loadcwd]", _cwd2)

    return nil
}

// CommonMistake2 -- CommonMistake1の間違い修正パターン (1)
func CommonMistake2() error {
    if err := loadcwd2(); err != nil {
        return err
    }

    output.Stdoutl("[main]", _cwd2)

    return nil
}
$ make run
ENTER EXAMPLE NAME: scope_common_mistake2
[Name] "scope_common_mistake2"
[loadcwd]            /home/devlights/dev/try-golang
[main]               /home/devlights/dev/try-golang


[Elapsed] 135.333µs

別の名前か型で定義しておいて、後で設定する

package scope

import (
    "os"

    "github.com/devlights/gomy/output"
)

type (
    _pkginfo struct {
        cwd string
    }
)

var (
    pkginfo _pkginfo
)

func loadcwd3() error {
    cwd, err := os.Getwd()
    if err != nil {
        return err
    }

    pkginfo.cwd = cwd
    output.Stdoutl("[loadcwd]", pkginfo.cwd)

    return nil
}

// CommonMistake3 -- CommonMistake1の間違い修正パターン (2)
func CommonMistake3() error {
    if err := loadcwd3(); err != nil {
        return err
    }

    output.Stdoutl("[main]", pkginfo.cwd)

    return nil
}
ENTER EXAMPLE NAME: scope_common_mistake3
[Name] "scope_common_mistake3"
[loadcwd]            /home/devlights/dev/try-golang
[main]               /home/devlights/dev/try-golang


[Elapsed] 191.917µs

おすすめ書籍

自分が読んだGo関連の本で、いい本って感じたものです。

Go言語による並行処理

Go言語による並行処理

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)

  • 作者:松尾 愛賀
  • 発売日: 2016/04/15
  • メディア: 単行本(ソフトカバー)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)


過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com