标签 Python 下的文章

也谈Go语言编程 – Hello,Go!

Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It's a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.
                                                                                          – 摘自Go语言官方站点

对于一门编程语言最深刻的喜欢莫过于对这门编程语言的设计理念的认同了,Go语言是继C语言之后又一门让我有如此感觉的编程语言。

初听到这门语言的存在时,我皱了皱眉:怎么起了这么一个名字!绝大多数编程语言都以名词或人名命名(如C、Java、PythonRubyHaskell、Ada等),而这门语言却用了一个日常生活中使用极为频繁的动词Go作为名字,这似乎有些太大众化了。不知为何,这个名字总是让 我联想到以前中国农村给小孩子常起的几个名字:二狗、牛娃等^_^。况且之前已经有很多IT产品也以Go作为名字(例 如,Thoughtworks公司出品的敏捷管理工具也叫Go)。

不过随着对这门语言的了解的深入,名字已不再是问题了。Go语言对我这个C程序员产生了强大的吸引力,原因如下:

* Go保持了与C语言一脉相承的理念:短小精悍、致力于成为系统编程语言、简洁而熟悉的C语言家族语法、静态编译型语言、保留了指针、运行高效;
* Go填平了C语言与生俱来的为数不少的"坑";
* Go提升了编译速度,统一了源码组织、构建规范以及编码规范,让程序员更集中精力于问题域;
* Go改进了并发模型,在语言级别原生支持多核平台;
* Go语言起点高,以创新的设计以及甚小的代价兼容了现有主流编程范型(例如OO等)。

因此有人称Go为21世纪的C语言,我觉得不为过。从这篇文章开始,我将和大家一起走入Go语言的世界。

一、安装Go

Go语言官方站(从国内访问十分不稳定,时能时不能,原因你懂的)对Go安装有着较为详尽的说明。如果你使用的是Linux、Mac OS或Windows,那你应该可以很顺利地完成Go的安装。Go于今年上旬发布了第一个稳定版本Go 1,目前最新版本是1.0.2,可以从Google Code上的Go项目中下载。我的环境为Ubuntu 10.04 32-bit,下载go1.0.2.linux-386.tar.gz后,解压到/usr/local/go下面:

$ ls /usr/local/go
api/     bin/           doc/        include/  LICENSE  PATENTS    README        src/   VERSION
AUTHORS  CONTRIBUTORS  favicon.ico  lib/      misc/    pkg/    robots.txt  test/

然后将/usr/local/go/bin添加到你的PATH环境变量中,你就可以在任意目录下执行go程序了:

$ go version
go version go1.0.2

如果你得到上面的输出结果,可以断定你的Go安装成功了!

二、第一个Go程序 – Hello, Go!

我们建立一个用于编写Go程序的工作目录go-examples,其绝对路径为/home/tonybai/go-examples。好了,开始 编写我们的第一个Go程序。

我们在go-examples下创建一个文件hellogo.go,其内容如下:

package main

import (
    "fmt"
)

func main() {
    fmt.Printf("Hello, Go!\n")
}

下面我们来编译该源文件并执行生成的可执行文件:

$ go build hellogo.go
$ ls
hellogo*  hellogo.go
$ hellogo
Hello, Go!

通过go build加上要编译的Go源文件名,我们即可得到一个可执行文件,默认情况下这个文件的名字为源文件名字去掉.go后缀。当然我们也 可以通过-o选项来指定其他名字:

$ go build -o myfirstgo hellogo.go
$ ls
myfirstgo*  hellogo.go

如果我们在go-examples目录下直接执行go build命令,后面不带文件名,我们将得到一个与目录名同名的可执行文件:

$ go build
$ ls
go-examples*  hellogo.go

三、程序入口点(entry point)和包(package)

Go保持了与C家族语言一致的风格:即目标为可执行程序的Go源码中务必要有一个名为main的函数,该函数即为可执行程序的入口点。除此之外 Go还增加了一个约束:作为入口点的main函数必须在名为main的package中。正如上面hellogo.go源文件中的那样,在源码第 一行就声明了该文件所归属的package为main。

Go去除了头文件的概念,而借鉴了很多主流语言都采用的package的源码组织方式。package是个逻辑概念,与文件没有一一对应的关系。 如果多个源文件都在开头声明自己属于某个名为foo的包,那这些源文件中的代码在逻辑上都归属于包foo(这些文件最好在同一个目录下,至少目前 的Go版本还无法支持不同目录下的源文件归属于同一个包)。

我们看到hellogo.go中import一个名为fmt的包,并利用该包内的Printf函数输出"Hello, Go!"。直觉告诉我们fmt包似乎是一个标准库中的包。没错,fmt包提供了格式化文本输出以及读取格式化输入的相关函数,与C中的printf或 scanf等类似。我们通过import语句将fmt包导入我们的源文件后就可以使用该fmt包导出(export)的功能函数了(比如 Printf)。

在C中,我们通过static来标识局部函数还是全局函数。而在Go中,包中的函数是否可以被外部调用,要看该函数名的首母是否为大写。这是一种 Go语言固化的约定:首母大写的函数被认为是导出的函数,可以被包之外的代码调用;而小写字母开头的函数则仅能在包内使用。在例子中你也看到了 fmt包的Printf函数其首母就是大写的。

四、GOPATH

我们把上面的hellogo.go稍作改造,拆分成两个文件:main.go和hello.go。

/* hello.go */
package hello

import "fmt"

func Hello(who string) {
    fmt.Printf("Hello, %s!\n", who)
}

/* main.go */
package main

import (
    "hello"
)

func main() {
    hello.Hello("Go!")
}

用go build编译main.go,结果如下:

$ go build main.go
main.go:4:2: import "hello": cannot find package

编译器居然提示无法找到hello这个package,而hello.go中明明定义了package hello了。这是怎么回事呢?原来go compiler搜索package的方式与我们常规理解的有不同,Go在这方面也有一套约定,这里面涉及到一个重要的环境变量:GOPATH。我们可以使用go help gopath来查看一下有关gopath的manual。

Go compiler的package搜索顺序是这样的,以搜索hello这个package为例:

* 首先,Go compiler会在GO安装目录(GOROOT,这里是/usr/local/go)下查找是否有src/pkg/hello相关包源码;如果没有则继续;
* 如果export GOPATH=PATH1:PAHT2,则Go compiler会依次查找是否存在PATH1/src/hello、PATH2/src/hello;配置在GOPATH中的PATH1和PATH2被称作workplace;
* 如果在上述几个位置均无法找到hello这个package,则提示出错。

在本例子中,我们尚未设置过GOPATH环境变量,也没有建立类似PATH1/src/hello这样的路径,因此Go compiler显然无法找到hello这个package了。我们来设置一下GOPATH变量并建立相关目录:

$ export GOPATH=/home/tonybai/go-examples
$ mkdir src/hello
$ mv hello.go src/hello
$ go build main.go
$ ls
main*  main.go    src/
$ main
Hello, Go!

五、Go install

我们将main.go移到src/main中,这样这个demo project显得更加合理,所有源码均在src下:

$cd src
$ ls
hello/    main/

Go提供了install命令,与build命令相比,install命令在编译源码后还会将可执行文件或库文件安装到约定的目录下。我们以main目录为例:

$ cd main
$ go install

install命令执行后,我们发现main目录下没有任何变化,原先build时产生的main可执行文件也不见了踪影。别急,前面说过Go install也有一套自己的约定:
* go install(在src/DIR下)编译出的可执行文件以其所在目录名(DIR)命名
* go install将可执行文件安装到与src同级别的bin目录下,bin目录由go install自动创建
* go install将可执行文件依赖的各种package编译后,放在与src同级别的pkg目录下

现在我们来看看bin目录:
$ ls /home/tonybai/go-examples
bin/  src/ pkg/
$ ls bin
main*

的确出现一个bin目录,并且刚刚编译的程序main在bin下面。

hello.go编译后并非可执行程序,在编译main的同时,由于main依赖hello package,因此hello也被关联编译了。这与单独在hello目录下执行install的结果是一样的,我们试试:

$ cd hello
$ go install
$ ls /home/tonybai/go-examples
bin/  pkg/  src/

在我们的workspace(go-examples目录)下出现了一个pkg目录,pkg目录下是一个名为linux_386的子目录,其下面有一个文 件:hello.a。这就是我们install的结果。hello.go被编译为hello.a并安装到pkg/linux_386目录下了。

.a这个后缀名让我们想起了静态共享库,但这里的.a却是Go独有的文件格式,与传统的静态共享库并不兼容。但Go语言的设计者使用这个后缀名似乎是希望 这个.a文件也承担起Go语言中"静态共享库"的角色。我们不妨来试试,看看这个hello.a是否可以被Go compiler当作"静态共享库"来对待。我们移除src中的hello目录,然后在main目录下执行go build:

$ go build
main.go:4:2: import "hello": cannot find package

Go编译器提示无法找到hello这个包,可见目前版本的Go编译器似乎不理pkg下的.a文件。http://code.google.com/p/go/issues/detail?id=2775 这个issue也印证了这一点,不过后续Go版本很可能会支持链接.a文件。毕竟我们在使用第三方package的时候,很可能无法得到其源码,并且在每个项目中都保存一份第三方包的源码也十分不利于项目源码的后期维护。

六、像脚本一样运行Go源码

Go具有很高的编译效率,这得益于其设计者对该目标的重视以及设计过程中细节方面的把控,当然这不是本文要关注的话题。正是由于go具有极速的编译,我们才可以像使用运行脚本语言那样使用它。

目前Go提供了run命令来直接运行源文件。比如:

$ go run main.go
Hello, Go!

go run实际上是一个将编译源码和运行编译后的二进制程序结合在一起的命令。但目前go源文件尚不支持作成Shebang Script,因为Go compiler尚不识别#!符号,下面的源码文件运行起来会出错:

#! /usr/local/go/bin/go run

package main

import (
    "hello"
)

func main() {
    hello.Hello("Go!")
}

$ go run main.go
package :
main.go:1:1: illegal character U+0023 '#'

不过我们可以可借助一些第三方工具来运行Shebang Go scripts,比如gorun

七、测试Go程序

前面说过Go起点较高,因此其自身就提供了一个轻量级单元测试框架包以及运行测试集的命令。

我们用一个例子来说明如何编写包的测试代码以及如何运行这个测试。我们在go-examples/src下建立另外一个目录mymath,mymath目录下mymath包的代码如下:

/* mymath.go */
package mymath

func MyAdd(i int, j int) int {
    return i + j
}

要对mymath包进行测试,我们需在同一目录下创建mymath_test.go文件,其中对MyAdd函数的测试代码如下:

/* mymath_test.go */
package mymath

import "testing"

func TestMyAdd(t *testing.T) {
    a, b := 4, 2
    if x := MyAdd(a, b); x != 6 {
        t.Errorf("MyAdd(%d, %d) = %d, want %d", a, b, x, 6)
    }
}

在这个文件中我们import了Go提供的标准单元测试包-testing,并且每个测试方法都已Test作为前缀开头。现在我们来运行一下这个测试,在mymath目录下运行go test命令:

$ go test
PASS
ok      mymath    0.007s

如果用例出错,我们就可看到下面提示:

$go test
— FAIL: TestMyAdd (0.00 seconds)
    mymath_test.go:8: MyAdd(4, 2) = 6, want 6
FAIL
exit status 1
FAIL    mymath    0.007s

由上可以看出,Go test也有自己的一些约定:测试源文件的名字必须以_test.go作为结尾;测试代码与被测代码在同一个包中;测试代码要导入testing包;测试 函数要以Test作为前缀,并且测试函数的函数签名必须是这样的:func TestXXX(t *testing.T)。

语言自带对测试的支持的好处是一致性,避免了大家使用不同的测试框架而给阅读、交流和维护带来的不便。

八、项目源码组织

有了源码、有了对编译原理的理解、有了测试框架的支持,我们就可以策划项目源码组织形式了。不过Go的诸多约定基本上已经将我们限制在如下结构上:

proj1/
    bin/
        myapp1*
    pkg/
        linux_386/
            lib1.a
            lib2.a
    src/
        lib1/
            lib1.go     
            lib1_test.go
        lib2/
            lib2.go     
            lib2_test.go
        … …
        myapp1/
            main.go       # main package source
            main_test.go  # test source

proj2/
    bin/
        myapp2*
    pkg/
        linux_386/
            lib3.a
            lib4.a
    src/
        lib3/
            lib3.go     
            lib3_test.go
        lib4/
            lib4.go     
            lib4_test.go
        … …
        myapp2/
            main.go       # main package source
            main_test.go  # test source

基于上述结构,我们可将GOPATH设置为proj1_path:proj2_path

九、代码风格(coding style)

Go程序员可以不再纠结于到底使用哪种代码风格,因为Go已经将代码风格做了严格的约定,一旦违反,Compiler直接给出Error。go还提供了fmt命令来协助Go程序员按标准格式化源文件。

从上面例子中我们可以看到Go的几大风格特点是:

* 左大括号'{'一定在函数名或if等语句在同一行
   func foo {

   }

* 无需显式用分号;将语句分隔(除非是在一行写上多条语句),因为compiler会替大家在适当位置加入分号的。
   i, j := 2, 3
   MyAdd(i, j)

   if x := MyAdd(a, b); x != 6 {
            … …
   }

* if、for等后面的表达式无需用小括号括上
  
   if x != 5 {
            … …
   }

十、查看文档

Go的全量文档几乎与Go安装包一起发布。安装Go后,执行godoc –http=:端口号即可启动doc server。打开浏览器,输入http://localhost:端口号即可以看到几乎与Go官方站完全相同的文档页面。

十一、参考书籍

Go毕竟是新生代语言,其自身尚不成熟和完善,资料也较少。这里推荐两本市面上比较好的且内容已更新到Go 1的书籍:

* Mark Summerfield的《Programming in Go: creating applications for the 21st century
* Ivo Balbaert的《The Way to Go – A Thorough Introduction to the Go Programming Language

buildc 0.1.9版本发布

随着buildc使用的深入,越来越多的新需求暴露了出来。为了满足这些需求,我们组的小兄弟又对buildc进行了一些改造,这些变化如下:

1、支持将多个子工程打包到一个安装包中

最初buildc的设计思想是为每个子工程单独制作安装包,这样具有很强的灵活性。但在对现有N个工程进行构建脚本改造的过程中发现,有些工程间存在严重 依赖,比如工程A是一个业务级公共库工程,工程B和工程C都依赖工程A构建后生成的静态共享库。而工程A又无法被当成第三方库处理,这给我们的安装包构建 制造了难题。我们的解决方法就是改造安装包工程的setup.cfg文件,让其支持多source。从正规语义上来讲,我们这么做将使得buildc支持 将多个子工程打包到一个安装包中,而间接的作用则是解决了上述有依赖关系的工程安装包制作的问题,虽然看起来不那么美。

修改后,setup.cfg中配置就变成下面这种情况了:

source = [
  {
     "trunk"          : "svn://10.10.15.56:4444/cn/trunk/A",
     "binary_prefix"  : "fooA"
  },

  {
    "trunk"          : "svn://10.10.15.56:4444/cn/trunk/B",
    "binary_prefix"  : "fooB"
  }
]

不过问题并没有彻底解决。A工程的构建结果是一个.a文件,按照原buildc处理流程,该.a文件会被放入安装包中的app目录下,但这不是我们想要 的,.a文件是不应该放入安装包的。与此同时,一些子工程的输出是.so文件,按照最初的规划,这些.so文件应该被放入deps目录,而原buildc 默认都是放在app目录下。为了解决这个问题,我们给source做了"属性扩容" – 增加了一个可选的pack_path字段。

source = [
  {
     "trunk"          : "svn://10.10.15.56:4444/cn/trunk/A",
     "binary_prefix"  : "fooA"
     "pack_path" : ""
  },

  {
    "trunk"          : "svn://10.10.15.56:4444/cn/trunk/B",
    "binary_prefix"  : "fooB"
  },

  {
    "trunk"          : "svn://10.10.15.56:4444/cn/trunk/C",
    "binary_prefix"  : "fooC"
    "pack_path" : "deps"
  }
]

这个示例中,A工程pack_path的值为"",buildc将忽略A工程的输出文件,B工程没有配置pack_path,则buildc默认将B工程 的输出文件放入app目录;而C工程明确为pack_path赋了值,buildc会将C工程输出文件放入deps目录。

buildc原本支持在命令行输入工程源码库地址(–tag=YOUR_SOURCE_TAG),现在依旧支持。一旦命令行指定了工程地 址,buildc将不会理睬setup.cfg中source中配置的各个源码库路径,将从命令行指定的工程地址处取得最新源码,但暂无法指定 pack_path,默认会将构建结果放入app目录。

2、打包时自动生成VERSION文件

buildc在执行pack build后,自动在安装包中生成一个VERSION文件。该文件中包含与安装包匹配的平台信息(包括CPU体系、OS类型等)以及构建时的一些版本信 息。该文件除了便于安装包使用人员查看安装包相关版本信息之外,还可以用作安装包在目标平台进行约束检测的依据。如果你的安装包是for x86-64 linux的,那么宕操作人员误将该安装包部署到Solaris 10 for sparc平台时,安装包中的deps_check.py脚本会比对当前平台信息与VERSION文件中携带的信息,发现不匹配,则报错并停止程序的安 装。

3、其他

该版本buildc还提供了一些脚本的详细示例,如env_gen.py、deps_check.py。另外从Make.rules.in模板中去除了硬编码的-D_DEBUG定义。

buildc 0.1.8版本发布

buildc这个小工具逐渐在项目组内部扩大了使用范围,还有一名专门的同事负责为每个项目制作安装包工程,这样也可以在使用中发现buildc的问题。

本次buildc 0.1.8的相关修正以及新增的feature就是我的这位年轻同事一手操刀完成的,他也是一个python新手,同样也是边翻手册边进行编码的。这次改动主要集中在templates目录下的几个文件,这里的文件多为因工程的不同而异的。

这次buildc主要的功能点改动如下:

1、删除Make.rules模板中的FOPTIMIZE变量

原先在模板中将FOPTIMIZE变量的值写死为o2。但在实际应用中,不是所有项目都会使用o2优化级别,通过在buildc.cfg中自定义变量也可以达到同样的效果,因此这里删除了该变量。

2、为setup.py.in增加了backup功能、log facility等

setup.py.in这个文件改动较大,主要包括:

- 在setup.py.in这个安装包模板中增加了backup命令,用于将目标服务器上运行的老版本应用环境进行打包备份处理。该命令支持两个参数all和conf,分别用于备份打包全部环境和打包配置文件目录;

- 将setup.py中原install命令的参数full改为'all';

- 为setup.py的执行过程增加了log facility,可以在"install_时间戳.log"中看到所有详细的安装过程;

- 当目标路径存在与安装包要安装的文件同名的文件时,setup.py.in会自动生成这两个同名文件的diff,供安装人员后续手动进行冲突解决。

3、提供一个deps_check.py的更为详尽的参考实现

deps_check.py是用于在目标环境进行环境约束检测的,十分必要。




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

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

如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多