标签 godoc 下的文章

理解Golang语句中的求值顺序

Golang在变量声明、初始化以及赋值语句上照比C语言有了许多改进:

a) 支持在同一行声明多个变量

var a, b, c int

b) 支持在同一行初始化多个变量(不同类型也可以)

var a, b, c = 5, "hello", 3.45
a, b, c := 5, "hello", 3.45 (short variable declaration)

c) 支持在同一行对多个变量进行赋值(在声明后且不同类型也可以)

a, b, c = 5, "hello", 3.45

这种语法糖我们是笑纳的,毕竟人生苦短,少写一行是一行啊^_^。

但这种语法糖却给我们带来了一些令人困惑的问题!比如下面这个就是Rob Pike在一个talk中slide(Go Course Day2)中的一个问题:

n0, n1 = n0 + n1, n0

or:

n0, n1 = op(n0,n1), n0

n0, n1的值在上述语句执行完毕后到底为多少呢?

显然这个问题涉及到Go语言的语句求值顺序(evaluation order)。求值序在任何一门编程语言中都是比较难缠的,很多情形下,语言规范给出的答案都是“undefined(未定义)” or "not specified" or “依赖实现”,尤其是对于哪些模棱两可的写法,就如Rob Pike给出的那个问题。

我们要想搞清楚Go中的求值顺序,我们需要求助于Go language specification,Go spec与Go发行版一起发布,你可以启动一个godoc web server(godoc -http=:6060,然后访问localhost:6060/ref/spec)查看go language specification。Go language specification专门有一个小节/ref/spec#Order_of_evaluation对求值顺序做了说明。

在Go specs中,有这样三点陈述:

1、变量声明(variable declaration)中的初始化表达式(initialization expressions)的求值顺序(evaluation order)由初始化依赖(initialization dependencies)决定;但对于初始化表达式内部的操作数的求值需要按照2中的顺序:从左到右;
2、在非变量初始化语句中,对表达式、赋值语句或返回语句中的操作数进行求值时,操作数中包含的函数(function)调用、方法(method)调用和通信操作(主要针对channel)将按语法从左到右的顺序求值。
3、赋值语句求值分为两个阶段,第一阶段是等号左边的index expressions、pointer indirections和等号右边的表达式中的操作数的求值顺序按照2中从左到右的顺序;第二阶段按从左到右的顺序对变量赋值。

下面我们就分别理解一下这三点。

一、变量声明中初始化表达式的求值顺序

带初始化表达式的变量声明的形式如下:

var a, b, c = expr1, expr2, expr3 //包级别或函数/方法内部

or 

a, b, c := expr1, expr2, expr3 //仅函数/方法内部

根据lang specs说明,求值顺序是由初始化依赖(initialization dependencies)规则决定的。那初始化依赖规则是什么呢?在Golang specs中也有专门章节说明:ref/spec#Package_initialization。

初始化依赖规则总结一下,大致有如下几条:

1、包中,包级别变量的初始化顺序按照声明先后的顺序,但如果某个变量(比如a)的初始化表达式中依赖其他变量(比如b),那么变量a的初始化顺序在变量b后面。
2、对于未初始化的,且不含有对应初始化表达式或其初始化表达式不依赖任何未初始化变量的变量,我们称之为"ready for initialization"变量。初始化就是按照声明顺序重复执行对下一个变量的初始化过程,直到没有"ready for initialization"变量为止。
3、如果初始化过程完毕后依然有变量处于未初始化状态,那程序有语法错误。
4、多个处于不同文件中的变量的声明顺序依赖编译器处理文件的顺序,先处理的文件中的变量的声明顺序先于后处理的文件中的所有变量。
5、依赖分析以包为单位执行,只有位于同一个包中的被依赖的变量、函数、方法才会被考虑。

规则是抽象难懂的,例子更直观易理解,我们看一个golang spec中的例子,并使用上述规则进行分析。实验环境:go 1.5, amd64,Darwin Kernel Version 13.1.0

//golang-statements-evaluating-order/example1.go
package main

import "fmt"

var (
    a = c + b
    b = f()
    c = f()
    d = 3
)

func f() int {
    d++
    return d
}

func main() {
    fmt.Println(a, b, c, d)
}

我们来分析一下程序执行后的a, b, c, d四个变量的结果值,不过不同的初始化顺序会导致结果值不同,因此分析四个变量的初始化顺序是至关重要的。

变量a, b, c, d的初始化过程如下:

1、根据规则,初始化按照变量声明先后顺序进行,因此先来分析变量a,a初始化表达式依赖b 和c;因此变量a的初始化次序排在b、c的后面;
2、按照a的初始化右值表达式,c、b在右值表达式中的出现顺序是c先于b;
3、c是否是一个ready for initialization变量呢?我们看到c依赖f这个函数,而f这个函数则依赖变量d的初始化,因此d排在c之前;
4、我们来看变量d,"d = 3",d未初始化且不含有初始化表达式,因此d是一个ready for initialization变量,我们可以从d开始初始化了。至此四个变量的初始化顺序排定 d -> c -> b -> a;(这块儿与spec中分析有差异,但从运行结果来看,应该是这个顺序;关于这个spec的issue参见#12369)
5、d初始化为3,此时已初始化变量集合[d=3];
6、接着初始化c:c = f(),因此c = 4(此时d=4),此时已初始化变量集合[c=4,d=4];
7、接下来轮到b:b = f(),因此b = 5 (此时d = 5),此时已初始化变量集合[b=5,c=4,d=5];
8、最后初始化a: a = c + b,在已初始化变量集合中我们可以找到b和c,因此a= 9,这样四个变量到此均已初始化;
9、经过分析:程序执行的结果应该是9,5,4,5。

我们来执行一下这个程序,验证一下我们的分析结果是否正确:

$go run example1.go
9 5 4 5

我们再来看一个例子,也是golang specs中的例子,我们稍作改造,并把它设定为example2:

//golang-statements-evaluating-order/example2.go
package main

import "fmt"

var a, b, c = f() + v(), g(), sqr(u()) + v()

func f() int {
    fmt.Println("calling f")
    return c
}

func g() int {
    fmt.Println("calling g")
    return a
}

func sqr(x int) int {
    fmt.Println("calling sqr")
    return x * x
}

func v() int {
    fmt.Println("calling v")
    return 1
}

func u() int {
    fmt.Println("calling u")
    return 2
}

func main() {
    fmt.Println(a, b, c)
}

同样根据变量初始化依赖规则对这个例子进行分析:

1、按照变量声明顺序,先初始化a:a= f() + v(),f()依赖变量c;v不依赖任何变量,因此变量c的初始化顺序应该在a变量前:c -> a。
2、分析c:c = sqr(u()) + v();u、sqr、v三个函数不依赖任何变量,因此c处于ready for initialization,于是对c进行初始化,函数执行顺序(从左到右)为:u() -> sqr() -> v(); 此时已初始化变量集合:[c = 5];
3、回到a:a = f() + v(),c初始化后,a也处理ready for initialization,于是对a初始化,函数执行顺序为:f() -> v(),此时已初始化变量集合:[c=5, a= 6];
4、按照变量声明次序,接下来轮到变量b:b= g(),而g()依赖a,a已经初始化完毕了,因此b也是ready for initialization,于是对b初始化,函数执行次序为:g(),至此已初始化变量集合:[c=5, a=6, b=6]。
5、经过分析:程序执行的结果应该是6,6,5。

我们来执行一下这个程序,验证一下我们的分析结果是否正确:

$go run example2.go
calling u
calling sqr
calling v
calling f
calling v
calling g
6 6 5

二、非变量初始化语句中的求值顺序

前面提到过:在非变量初始化语句中,对表达式、赋值语句或返回语句中的操作数进行求值时,操作数中包含的函数(function)调用、方法(method)调用和通信操作(主要针对channel)将按语法从左到右的顺序求值

我们同样来看一个例子:example3.go

//golang-statements-evaluating-order/example3.go
package main

import "fmt"

func f() int {
    fmt.Println("calling f")
    return 1
}

func g(a, b, c int) int {
    fmt.Println("calling g")
    return 2
}

func h() int {
    fmt.Println("calling h")
    return 3
}

func i() int {
    fmt.Println("calling i")
    return 1
}

func j() int {
    fmt.Println("calling j")
    return 1
}

func k() bool {
    fmt.Println("calling k")
    return true
}

func main() {
    var y = []int{11, 12, 13}
    var x = []int{21, 22, 23}

    var c chan int = make(chan int)
    go func() {
        c <- 1
    }()

    y[f()], _ = g(h(), i()+x[j()], <-c), k()

    fmt.Println(y)
}

y[f()], _ = g(h(), i()+x[j()], <-c), k() 这行语句是赋值语句,但赋值语句的操作数中包含函数调用、channel操作,按照规则,这些函数调用、channel操作按从左到右顺序估值。

1、按照从左到右顺序,第一个是y[f()]中的f();
2、接下来是g(),g()的参数列表依然是一个赋值操作,因此其涉及到的函数调用顺序为h(), i(),j(),<-c,因此实际上的顺序为h() –> i()–> j() –> c操作 -> g();
3、最后是k(),因此完整的调用顺序是:f()->
h() –> i()–> j() –> c操作 -> g() –> k()。

实际运行情况如下:

$go run example3.go
calling f
calling h
calling i
calling j
calling g
calling k
[11 2 13]

三、赋值语句的求值顺序

我们再回到前面Rob Pike那个问题:

n0, n1 = n0 + n1, n0

or:

n0, n1 = op(n0, n1), n0

这是一个赋值语句,根据规则3,我们对等号两端的表达式的操作数采用从左到右的求值顺序。

我们假定初值:
n0, n1 = 1, 2

1、第一阶段:等号两端表达式求值,上述问题中,只有右端有n0+n1和n0两个表达式,但表达式的操作数(n0,n1)都是初始化过后的了,因此直接将值带入,得到求值结果。求值后,语句可以看成:n0, n1 = 3, 1;
2、第二阶段:赋值。n0 =3, n1 = 1

//golang-statements-evaluating-order/example4.go
package main

import "fmt"

func example1() {
    n0, n1 := 1, 2
    n0, n1 = n0+n1, n0
    fmt.Println(n0, n1)
}

func op(a, b int) int {
    return a + b
}

func example2() {
    n0, n1 := 1, 2
    n0, n1 = op(n0, n1), n0
    fmt.Println(n0, n1)
}

func main() {
    example1()
    example2()
}

$go run example4.go
3 1
3 1

四、小结

虽说理解了规则,但实际工作中我们还是尽量不要写出像:"var a, b, c = f() + v(), g(), sqr(u()) + v()"这样复杂、难以让人理解的语句。必要的话,拆分成多行就好了,还可以增加些代码量(如果你的公司是以代码量为评价绩效指标之一的),得饶人处且饶人啊,烧脑的语句还是尽量避免为好。

以上实验代码在这里可以下载到。

五、参考资料

The Go Programming Language Specification (Version of August 5, 2015) 

制作go-talks.appspot.com应用镜像

Go语言号称面向工程:对工程目录组织、代码风格(gofmt)、文档(生成)都制定的相应的“标准”,并提供了相应的工具帮助开发者满足这些工程specs。

gofmt用于格式化代码,形成统一代码风格。
godoc.org用于查看标准库或repo的doc。
go-talks.appspot.com则是用来查看go slide。

godocgo-talks这种以服务形式提供文档查看的形式不得不说是golang的又一创新。

这几年Golang的开发者们是非常勤奋的,为了推广Golang,他们撰写博客,编写文档,并四处布道,积累下许多有价值的文档,这些文档多以 Gopher所特有的present格式存在着,这些 present格式的文档以.slide、.article或.ext为后缀,通过go-talks.appspot.com提供的present渲染服 务浏览,并且支持github.com repo中的slide文件。Go开发者们只需要将自己写好的slide文件存放在自己github.com上的repo中,就可以随时随地在世界各地打 开这类present文件为大家布道了。

不过来到中国大陆后,事情就没那么顺利了,因为appspot.com在大陆是无法直接访问的,你懂得哦。为了观看这些大牛的slide,内地的Go程序员只能四处寻找出(fan)国(qiang)工具,但这毕竟不是十分方便。

上周末@开发者头条分享了“why Go is fast? [Slide] High performance servers without the event loop (Golang)”这个Dave Cheney在O'Reilly OSCON上分享的Go slide,但因为链接被qiang,无法直接观看。于是就想到能不能制作一个go-talks.appspot.com的镜像站点,让国内Go程序员也 能享受些福利呢?于是乎我就开始了镜像制作的探索过程。

一、在本地搭建go-talks.appspot.com镜像

present格式类似于markup,是一种标记语言,只是present格式更多用来制作slide。

golang.org/x/tools/present提供了present文件格式的解析库,最初本以为需要从头开始写server,并利用 present库解析,写模板和javascript实现类似翻页等功能呢。但后来居然在gddo repo,也就是godoc.org的源码工程中找到了go-talks.appsport.com站点的源码: talksapp。

不过talksapp是运行在google app engine上的应用,要将其直接运行在standalone server上是否可行呢?是否需要改造?这些都是未知数,不过有了源码自然是很好的。我们先来试试这个程序是否能在本地运行起来。

首先下载gddo repo:

$go get github.com/golang/gddo/
$cd $GOPATH/src/github.com/golang/gddo/talksapp

talksapp的主页文档似乎有些out-dated,我并没有找到config.go.template。   

但按照文档要求,需要下载Go App Engine SDK,这个需要搭梯子。在https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Go页面根据您的平台版本下载最新Go SDK版本。解压后,先放在那里不动。

根据talksapp文档,第三步就应该是sh setup.sh。setup.sh中get两个repo均在qiang外,需要梯子才能下载。

setup.sh正确执行之后,我们用go_appengine下dev_appserver.py来运行talksapp:

$dev_appserver.py ~/Test/GoToolsProjects/src/github.com/golang/gddo/talksapp
INFO     2015-07-27 08:25:09,076 api_server.py:205] Starting API server at: http://localhost:51801
INFO     2015-07-27 08:25:09,080 dispatcher.py:197] Starting module "default" running at: http://localhost:8080
INFO     2015-07-27 08:25:09,083 admin_server.py:118] Starting admin server at: http://localhost:8000
/Users/tony/Test/GoToolsProjects/src/appengine/google/appengine/tools/devappserver2/mtime_file_watcher.py:115: UserWarning: There are too many files in your application for changes in all of them to be monitored. You may have to restart the development server to see some changes to your files.
  'There are too many files in your application for '
ERROR    2015-07-27 08:25:11,941 http_runtime.py:380] bad runtime process port ['']
2015/07/27 08:25:11 secret.json needs to define ClientID and ClientSecret

使用浏览器访问localhost:8080,得到的页面中也只是有些错误日志,日志与上面最后两行相同。从错误日志来看,似乎需要配置一下secret.json这个文件,至少ClientID和ClientSecret不能为空。

我就随意配置两个值(这两个值似乎应该是github.com的账号和密码,用于OAuth2,如果随意配置无法成功,那建议配置上真实的账号和密码),看看是否可以访问:

{
    "ClientID": "xx",
    "ClientSecret": "yy"
}

这回再执行talksapp就不再报错了。用浏览器访问localhost:8080, go-talks的页面顺利正常显示出来!看来在本地是可以运行的哦!

我们再来测试一下访问github.com上的一个slide,地址如下:

http://localhost:8080/github.com/gophercon/2015-talks/Dmitry_Vyukov_-_Go_Dynamic_Tools/tools.slide

加载有些慢,有些时候提示:
  
   canceled: Deadline exceeded (timeout)

试了几次后,居然加载成功了!又试了几个slide,除了有些慢,都是成功的。看来talksapp是可以在standalone主机上运行的。

二、在vps上部署go-talks镜像

虽然在本机上可以正常浏览Golang大牛们的slide的了,但毕竟放在local上不是很方便,离开这台机器又无法访问了。广大内地go程序员们依旧 生活在“水深火热”中,在“分享经济”兴起的今天,我想也力所能及的做些贡献吧。于是想到了将这个镜像部署到我的blog vps上,这样大家就可以自由浏览golang slide了。

我的vps放在了DigitalOcean上(Ubuntu 14.04 server amd64),配置较低,平时仅仅作为blog托管主机。不过放一个go-talks镜像应该还是可以满足的,也可以更充分“压榨”一下DO的资源。

于是乎,我就按照上面的步骤将talksapp安装在了vps上。考虑到talksapp作为一个守护进程,又安装了supervisor对其进行管理:

/etc/supervisor/conf.d/go-talks.conf
[program:go-talks]
environment=GOROOT=/root/.bin/go142
environment=GOPATH=/root/go-talks
directory=/root/go-talks/src/github.com/golang/gddo/talksapp
command=/root/go-talks/go_appengine/goapp serve
autostart=true
autorestart=true
startsecs=3

这里没有使用dev_appserver.py,而是用了两位一个程序goapp,通过在talksapp目录下执行goapp serve来启动这个"GAE"服务。现在vps上启动了localhost:8080服务,但外面的人还是无法访问到这个服务。

如果要对外发布这个服务,我需要一个域名,考虑到自己已有的blog域名,为了快速开通服务,我添加了一个二级域名:go-talks.tonybai.com,模仿go-talks.appspot.com。

我们还需要调整一下apache2 server。原先的apache2 server只是为blog(wordpress)提供服务,现在我们需要将go-talks.tonybai.com映射到主机内部的8080端口服务 上,这就需要开启apache2的反向代理功能,对apache2也不是很熟悉,于是在网上找到了一段配置,补充到/etc/apache2 /apache2.conf中:

<VirtualHost *:80>
    ServerName go-talks.tonybai.com
    ProxyPreserveHost On
    ProxyRequests Off
    ProxyPass / http://localhost:8080/
    ProxyPassReverse / http://localhost:8080/
</VirtualHost>

Include /etc/phpmyadmin/apache.conf

重启apache2,出现下面错误:

root@tonybai:/etc/apache2# sudo service apache2 restart
 * Restarting web server apache2          [fail]
 * The apache2 configtest failed.
Output of config test was:
AH00526: Syntax error on line 85 of /etc/apache2/apache2.conf:
Invalid command 'ProxyPreserveHost', perhaps misspelled or defined by a module not included in the server configuration
Action 'configtest' failed.
The Apache error log may have more information.

似乎是反向代理需要更多apache2 module才能运行,于是:

sudo a2enmod proxy
sudo a2enmod proxy_http

再重启apache2,这回ok了。

在DNS服务商内已经添加了go-talks.tonybai.com这个域名,但由于国内DNS生效时间较慢,为了测试服务是否ok,我修改了 hosts文件,手动将go-talks.tonybai.com指向vps的公网地址。接下来访问go-talks.tonybai.com这个地址, 镜像制作成功了! 又测试了几个slide,均正确生成!速度稍慢,那是因为vps的一般延迟都在2600ms左右。

我的VPS性能不高,大家访问时也许会感觉较慢,但有胜于无!

最后再重申一下go-talks.tonybai.com的使用方法:

如果某个分享链接为:go-talks.appspot.com/xxx/yy/zz/foo.slide,那么将该地址替换为:go- talks.tonybai.com/xxx/yy/zz/foo.slide即可。也就是将appspot换成tonybai,其他不变。

该服务已经利用监控宝监控起来了,如果出现问题(比如网络或资源不足的问题),我会及时处理。但这里不保证100%可用哦!希望大家友好使用,不要拍砖!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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