标签 scope 下的文章

对一段有关Go Code Block和变量作用域的代码的简要分析

近期,Go team的David CrawShawtwitter上贴出了一段代码,如下:

func main() {
    if a := 1; false {
    } else if b := 2; false {
    } else if c := 3; false {
    } else {
        println(a, b, c)
    }
}

David CrawShaw想表达的意图是gopher们很少在”else if”后面的simple statement中使用“短变量声明”形式,而这段代码是个例外。我们看到b、c两个变量都是在else if 的simple statement中使用短变量声明形式定义的。

我个人看到这段代码后,第一反应是:这段代码能编译运行吗?else语句中的“println(a, b, c)”是否会被compiler报出:undefined b, c的错误呢?不知道是否有其他的gopher们与我有同样的反应:)。无论怎样,既然有了疑问,我们就应该把它分析清楚。

一. Go代码块和作用域简介

根据Go语言的规范,我们知道Go的标识符作用域是基于代码块(code block)的。代码块就是包裹在一对大括号内部的声明和语句,并且是可嵌套的。我们在代码中直观可见的显式的(explicit)code block有很多,比如:函数的函数体、for循环的循环体等:

func Foo() {
    // here:显式的(explict block)代码块,包裹在函数的函数体内
   ... ...

    for {
            // here: 显式的(explict block)代码块,包裹在for循环体内
            // 该代码块就嵌套在函数体这个代码块的内部
        ... ...
    }
}

但除了显式explict的code block,Go语言中还有几种隐式的(implicit)代码块,它们都是什么呢?这里摘录下go spec原文(不翻译了):

1. The universe block encompasses all Go source text.
2. Each package has a package block containing all Go source text for that package.
3. Each file has a file block containing all Go source text in that file.
4. Each "if", "for", and "switch" statement is considered to be in its own implicit block.
5. Each clause in a "switch" or "select" statement acts as an implicit block.

我们看到if语句会引入一个隐式的code block,这为我们后续的分析奠定了基础。

二. if语句的code block

那么if语句的code block详细情况如何呢?我们分门别类地简单看看:

1. if _ 型

我们使用最多的if语句类型就是单if型,即:

if simplestmt; expression {
    ... ...
}

在这种类型的if语句中,有两个code block:一个隐式的code block和一个显式的code block。我们把上面的形式代码做一个等价变化,并加上code block起始和结束点的标注,结果如下:

{ // implicit block begin
    simplestmt

    if expression { // explicit block begin
            ... ...
    } // explicit block end

} // implicit block end

我们看到if后面的”大括号对”引入的explict code block嵌套在if simplestmt所在的implicit code block内部,这也是为何simplestmt中用短声明形式定义的变量在explict block中可以使用的原因:

func main() {
    if a := 1; true {
        fmt.Println(a) // output: 1
    }
}

2. if _ else _ 型

我们再来看看if _ else _ 型

if simplestmt; expression {
    ... ...
} else {
    ... ...
}

分析逻辑同上,我们将上面的伪代码做一个等价变换,并作出code block起始结束点标注:

{ // implicit block begin
    simplestmt

    if expression { // explicit block1 begin
            ... ...
    }  else  { // explicit block1 end, explicit block2 start
            ... ...
    } //explicit block2 end

} // implicit block end

我们看到if _ else _ 型 有三个code block,除了单if型的两个block外,还由else引入一个explict code block(即上面代码中的explict block2)。

3. if _ else if _ else _ 型

最后我们来看看最为复杂的if _ else if _ else _ 型

if simplestmt1; expression1 {
    ... ...
} else if simplestmt2; expression2 {
    ... ...
}  else {
    ... ...
}

我们依旧将上面的伪代码做一个等价变换,并作出code block起始结束点标注,结果如下:

{ // implicit block1 begin
    simplestmt1

    if expression { // explicit block1 begin
            ... ...
    }  else  { // explicit block1 end, explicit block2 start

            {  // implicit block2 begin
                simplestmt2

                if expression2 { //  explicit block3 start

                } else { // explicit block3 end, explicit block4 start

                } // explicit block4 end

            } // implicit block2 end

    } //explicit block2 end

} // implicit block1 end

我们看到在该类型下,我们一共识别出两个implict block和四个explict block。

三. 对David CrawShaw贴出的那段代码的分析

有了第二节的基础,再来看David CrawShaw的这段代码:

func main() {
    if a := 1; false {
    } else if b := 2; false {
    } else if c := 3; false {
    } else {
        println(a, b, c)
    }
}

依照我们的思路,我们可以对这段代码做一个等价变化:

func main() {
    {
        a := 1
        if false {

        } else {
            {
                b := 2
                if false {

                } else {

                    {
                        c := 3
                        if false {

                        } else {
                            println(a, b, c)
                        }
                    }
                }
            }
        }
    }
}

展开后的语句就很是一目了然了,不用说什么大家也会很清楚了。最重要的一点是原来代码中最后的那个else实际上是与最内层的else if配对的,而不是与最开始的if配对的,因此println(a, b, c)的时候,a, b, c三个变量都是已经声明定义了的(在外层的code block中)。

对于此类涉及code block或变量作用域的问题,还可以通过go vet -shadow工具来辨别,或通过go run执行后的出错信息来辨别,这里就不详细说明了。

四. 参考资料


51短信平台:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

一个有关Golang变量作用域的坑

临近下班前编写和调试一段Golang代码,但运行结果始终与期望不符,怪异的很,下班前依旧无果。代码Demo如下:

//testpointer.go
package main

import (
        "fmt"
)

var p *int

func foo() (*int, error) {
        var i int = 5
        return &i, nil
}

func bar() {
        //use p
        fmt.Println(*p)
}

func main() {
        p, err := foo()
        if err != nil {
                fmt.Println(err)
                return
        }
        bar()
        fmt.Println(*p)
}

这段代码原意是定义一个包内全局变量p,用foo()的返回值对p进行初始化,在bar中使用p。预期结果:bar()和main()中均输出5。但编译执行后的结果却是:

$go run testpointer.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x20d1]

goroutine 1 [running]:
main.bar()
    /Users/tony/Test/Go/testpointer.go:17 +0xd1
main.main()
    /Users/tony/Test/Go/testpointer.go:26 +0x11c

goroutine 2 [runnable]:
runtime.forcegchelper()
    /usr/local/go/src/runtime/proc.go:90
runtime.goexit()
    /usr/local/go/src/runtime/asm_amd64.s:2232 +0×1

goroutine 3 [runnable]:
runtime.bgsweep()
    /usr/local/go/src/runtime/mgc0.go:82
runtime.goexit()
    /usr/local/go/src/runtime/asm_amd64.s:2232 +0×1

goroutine 4 [runnable]:
runtime.runfinq()
    /usr/local/go/src/runtime/malloc.go:712
runtime.goexit()
    /usr/local/go/src/runtime/asm_amd64.s:2232 +0×1
exit status 2

晚饭后,继续调试这段代码。怎么还crash了!代码看似半点问题都没有,难道是Go编译器的问题,我用的可是最新的1.4,切换回1.3.3,问题依旧啊。看来还是代码的问题,但问题在哪里呢?加上些打印语句再看看:

func bar() {
        //use p
        fmt.Printf("%p, %T\n", p, p) //output:
0x14dc80, 0×0, *int
        fmt.Println(*p) //Crash!!!
}

func main() {
        fmt.Printf("%p, %T\n", p, p) //output: 0x14dc80, 0×0, *int
        p, err := foo()
        if err != nil {
                fmt.Println(err)
                return
        }
        fmt.Printf("%p, %T\n", p, p) //output: 0x2081c6020, 0x20818a258, *int
        bar()
        fmt.Println(*p)
}

通过打印输出,发现从foo函数中返回的p(0x2081c6020)与全局变量的p(0x14dc80)居然不是一个地址,也就是说不是一个变量。而且 从bar()中的调试输出来看,全局变量p在foo函数返回时并未被赋值为foo中变量i的地址,而依然是一个nil值,从而导致程序Crash。

好了,废话不说了,该是揭晓真相的时候了。问题就在于":="。在main这个作用域中,我们使用了

p, err := foo()

最初的理解是golang会定义新变量err,p为初始定义的那个全局变量。但实际情况是,对于使用:=定义的变量,如果新变量p与那个同名已定义变量 (这里就是那个全局变量p)不在一个作用域中时,那么golang会新定义这个变量p,遮盖住全局变量p,这就是导致这个问题的真凶。

我们将main函数改为:

func main() {
        var err error
        p, err = foo()
        if err != nil {
                fmt.Println(err)
                return
        }
        bar()
}

则执行结果就完全符合预期了。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats