标签 C 下的文章

也谈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

开始学Go

本文翻译自Dr.Dobb's的"Getting Going with Go"。

本文是有关Google新的系统原生语言的五周教程的第一部分,这里将先向大家展示如何建立Go语言开发环境以及构建程序,然后带领大家浏览 一些代码范例来着重了解一下这门语言的一些有趣的特性。

这个教程系列将连续刊登五周。在今天这一部分中,Go语言专家Mark Summerfield将讲解如何建立Go语言开发环境,提供两个Go语言范例并给予深度解析。这些样例程序会向大家局部地展示了Go语言的一些关键特性 以及包。接下来几周将展示其余的关键特性,并特别为C、C++和Java程序员们深入研究那些Go语言独有的特性。

正如本周主编文章中所解释的那样,Go语言拥有许多独一无二的特性,因此它也许可以被称为二十一世纪的C语言。而且考虑到Ken Thompson也是该语言的设计者之一,这两种语言的确是有共同的祖先。

开始

Go是编译型语言,而不是解释型的。Go的编译速度非常快– 甚至远远快过其他同类语言- 知名的如C和C++。

标准Go语言编译器被称为gc,与其相关的工具链包括用于编译的5g、6g和8g;用于链接的5l、6l和8l以及用于查看Go语言文档的 godoc(在Windows平台上这些程序为5g.exe、6g.exe等等诸如此类)。这些奇怪的名字遵循了Plan 9操作系统编译器的命名方式,即用数字表示处理器体系("5"代表ARM,"6"代表AMD64-包括Intel 64位处理器- "8"代表Intel 386)。幸运的是,我们无需对此产生忧虑,Go语言提供了一个更高级别的Go语言构建工具,这个工具可以为我们处理编译和链接任务。

本文中的所有代码使用的都是Go 1版本语法,并在Linux、Mac OS X以及Windows上用gc测试通过了。Go语言的开发者计划让随后所有Go 1.x版本支持Go 1向后兼容,因此这里的代码和例子将适用于所有1.x系列版本。

要下载和安装Go,请访问golang.org/doc/install.html,那里提供了下载链接与安装指令。Go 1为FreeBSD 7+、Linux 2.6+、Mac OS X (Snow Leopard和Lion)以及Windows 2000+提供源码包以及二进制形式安装包,可以支持所有Intel 32位和AMD 64位处理器体系。Go还支持ARM处理器版本的Linux,为Ubuntu Linux发布版提供预建go包。当你读到这里时,也许已经有其他Linux发布版的Go安装包了。

使用gc编译器的程序使用了一种特定的调用惯例(call convention)。这意味着使用gc编译的程序只可以与使用同样调用惯例编译的外部库进行链接 – 除非使用某适合的工具消除这些差异。使用cgo工具Go可以支持在Go程序中使用外部C代码,并且至少在Linux和BSD系统上,通过SWIG工具我们 可以将C和C++代码用于Go程序中。

除了gc,还有一种编译器称为gccgo。它是Gcc的一个Go特定前端,Gcc 4.6及以后版本才能支持。像gc一样,gccgo也许内建在一些Linux发行版中。构建和安装gccgo的指令在Go主站点上可以找到。

Go文档

Go的官方站点上维护着一份最新的Go文档。"Packages"链接提供了有关所有Go标准库包的访问方式- 以及它们的源码,这些源码在文档还很稀缺时十分有用。通过"Commands"链接你可以找到与Go一起发布的相关程序的文档(诸如编译器,构建工具 等)。通过"Specification"链接,你可以找到一份非正式,但很全面的Go语言规范。通过"Effective Go"链接,你可以找到一份介绍Go最佳实践的文档。

该站点还提供了一个沙箱特性,用于在线编写、编译以及运行Go小程序(稍有限制)。这个特性十分有用,便于初学者试验一些古怪的语法。Go站点上 的搜索框只能用于在Go文档中搜索;如果要对Go的资源进行全面搜索,请访问http://go-lang.cat-v.org/go- search。

Go的文档也可以在本地浏览,例如在Web浏览器中。如果要这样做,可运行Go的godoc工具,并通过传入命令行参数告知它以一个web服务器 的方式运行。下面是在一个Unix或Windows控制台中进行这个操作的方法,假设PATH环境变量中已经设置了godoc:

$ godoc -http=:8000

这个例子中的端口号可以是任意的- 如果它与一个已存在的服务器端口冲突,可以使用其他任一个端口号。

要想查看文档,打开一个浏览器,输入地址http://localhost:8000。一个类似golang.org首页的页面将会呈现在你的面 前。"Package"链接将指向Go标准库以及安装在GOROOT环境变量下的第三方包的文档。如果你定义了GOPATH环境变量(比如,为本 地程序和包),一个链接将会出现在"Packages"链接旁,通过这个链接你可以访问其他相关文档。(本文后续会讨论GOROOT和 GOPATH环境变量)

编辑,编译和运行

Go程序用UTF-8编码的普通的Unicode文本编写。绝大多数现代文本编辑器都可以自动处理这些代码,并且一些最流行的编辑器可以支持Go 源码的语法色彩高亮以及自动缩进。如果你的编辑器不支持Go,可以尝试在Go搜索引擎中输入你的编辑器的名字,查看一下是否有合适的插件。作为编 辑惯例,Go所有的关键字和操作符都使用ASCII字母;然而,Go的标识符可以以任意Unicode字母作为起始,后面可以跟着任意 Unicode字母或数字,因此Go开发者可以自由使用他们的母语。

为了掌握如何编辑、编译以及运行一个Go程序,我开始会用经典的"Hello World"程序作为例子- 然而我编写这个程序比寻常的稍复杂些。

如果你已经用二进制包或通过源码安装了Go,并且是以root或管理员权限安装的,你应该至少设置一个环境变量:GOROOT,该变量指示Go的 安装路径信息,你的PATH变量现在应该包含$GOROOT/bin或%GOROOT%\bin。为了检查Go安装地是否正确,可在控制台下输入 下面命令:

$ go version

如果你得到"command not found"或"'go' is not recognized…"的错误信息,那就意味着在PATH变量配置的路径下没有Go。

编译与链接

构建一个Go程序需要两步:编译和链接。(由于我们假设使用gc编译器,使用gccgo编译器的读者需要遵循golang.org/doc /gccgo_install.html中描述的编译和链接过程,同样,使用其他编译器的读者需要根据其编译器的指令进行编译和链接)。编译和链 接过程都由工具go处理,它不仅可以构建本地程序和包,还能够获取、构建以及安装第三方程序和包。

要想go能够编译本地程序和包,有三个要求。第一,Go的bin目录($GOROOT/bin或%GOROOT%\bin)必须在PATH环境变 量下。第二,必须存在一个目录,该目录下包含一个src目录,本地程序和包的源码就驻留在src目录下。例如,例子代码会解包到goeg/src /hello、goeg/src/bigdigits下等。最后,包含src的那个目录必须在GOPATH环境变量中设置。例如,要使用go工具 构建hello这个例子,我们必须这么做:

$ export GOPATH=$HOME/goeg
$ cd $GOPATH/src/hello
$ go build

两个例子中,我们都假设PATH环境变量中包含了$GOROOT/bin或%GOROOT%\bin。一旦go编译程序完毕,我们就可以运行这个 程序了。默认情况下,go会用可执行文件所在目录的名字来命名该文件(例如,在类Unix系统上是hello,而在Windows上为 hello.exe)。一旦构建完毕,我们就可以按常规方式运行它。

$ ./hello
Hello World!

注意,我们不需要编译或显式地链接其他包(即便后续我们将看到,hello.go使用了三个标准库的包)。这也是Go程序编译如此之快的一个原 因。

如果我们有多个Go程序,若他们的可执行文件能够放在同一个目录下将会非常方便,后续只需将这个目录加入到PATH环境变量中。幸运地是go支持 这样的情况:

$ export  GOPATH=$HOME/goeg
$ cd  $GOPATH/src/hello
$ go install

go install命令除了做了go build所做的事情之外,还将可执行文件放在标准位置($GOPATH/bin或%GOPATH%\bin)。这意味着将一个单一路径($GOPATH /bin或%GOPATH>%\bin)加入到PATH环境变量中,我们安装的所有Go程序就可以方便地被加入到PATH中。

除了这里给出的例子外,我们很可能想要在我们自己的目录下开发我们自己的Go程序和包。通过为GOPATH环境变量设置两个(或更多)冒号分隔的 路径(在Windows上用分号分隔)我们可以很容易解决这个问题。

虽然Go使用go工具作为标准构建工具,但你仍然可以使用make或其他现代构建工具,或使用其他Go专用的构建工具,或一些流行IDE的插件。

和谁打招呼(Hello)?

既然我们已经看到了如何构建一个Hello程序,接下来我们来看看其源代码。下面是hello程序的完整源码(在文件 hello/hello.go中):

// hello.go
package main
import (
    "fmt"
    "os"
    "strings"
)
func main() {
    who := "World!"
    if len(os.Args) > 1 { /* os.Args[0] is "hello" or "hello.exe" */
        who = strings.Join(os.Args[1:], " ")
    }
    fmt.Println("Hello", who)
}

Go用C++风格的注释符号//作为单行注释,用/* … */作为多行注释符号。依照惯例,Go中多使用单行注释,多行注释常用于在开发中注释掉代码块。

每段Go代码都存在于一个包内,并且每个Go程序必须具有一个包含main()函数的main包,其中main函数会作为程序执行的入口点,即这 个函数首先执行。事实上,Go包也可以定义在main函数之前执行的init函数。值得注意的是包名和函数名之间不会存在冲突的情况。

Go的操作是以包为单位的,而不是文件。这意味着我们可以根据需要任意地将一个包拆分到多个文件中。如果多个文件具有相同的包声明,Go语言认为 这些文件都是同一个包的组成部分,与所有内容在单一文件中无异。当然,我们也可以将应用的功能分解到许多本地包中,这样可以保持代码整洁地模块 化。

import语句从标准库导入三个包。fmt包提供格式化文本以及读取格式化文本的函数;os包提供平台无关的操作系统变量以及函 数;strings包提供操作字符串的函数。

Go的基本类型支持普通操作符(例如,+可用于数值加法以及字符串连接),Go的标准库通过提供操作基本类型的函数包补充功能,例如这里导入的 strings包。我们可以在基本类型的基础上创建我们自定义的类型并为它们定义相关方法- 自定义操作特性类型的函数。

读者也许已经注意到了Go源码中没有分号、import的包无需逗号分隔以及if条件语句不需要括号。在Go中,块(block),包括函数体以 及控制结构体(如for、if语句以及for循环),使用括号界定。缩进只是单纯用于提高代码的可读性。技术上而言,Go语句是用分号分隔的,但 这些分号由编译器插入,我们自己无需关心,除非我们要将多个语句放在同一行中时。没有分号、很少的逗号以及括号让Go程序看起来更简洁,需要的输 入也更少。

Go使用func关键字定义函数(function)和方法(method)。main包的main()函数总是具有相同的函数签名 – 没有参数、没有返回值。当main.main()结束时,程序将终止并返回0给操作系统。当然,我们可以在任意时刻返回并选择我们自己的返回值。

main()函数中的第一个语句(使用:=操作符)在Go的术语里被称为一个短变量声明。这个语句在同一时间声明并初始化一个变量。此外,我们无 需指定变量的类型,因为Go可根据初始值推导出变量的类型。因此在这个例子中,我们声明了一个string类型的变量who,感谢Go的强类型机 制,我们只需将字符串赋值给who即可。

os.Args变量是字符串的一个slice(片段)。Go使用array(数组)、slice和其他集合数据类型,但在这些例子中,知道下面这 些即可:使用内置的len()函数获取一个slice的长度以及通过[]下标操作符访问其中的元素。特别是,slice[n]返回slice的第 n个元素(从0起始),slice[:n]返回另外一个slice,这个新slice由原slice的第n个到最后一个之间的元素组成。在集合一 章,我们将看到有关这方面的一般性的Go语法。就这里的os.Args来说,这个slice总是应该至少在位置0处具有一个字符串(程序的名 字)。(所有Go的下标索引都是从0开始)

如果输入一个或更多命令行参数,if条件将被满足,我们将所有参数拼接成一个单一的字符串并赋值给who。在这里例子中,我们使用赋值操作符 (=),如果我们使用:=,我们将声明和初始化一个新的who变量,其影响范围将局限在if语句块中。strings.Join函数接受一个字符 串slice和一个分割符(可以为空,即"")作为参数,并返回一个由所有slice的字符串元素组成的,由分隔符分隔的单一字符串。这里我们用 空格分隔这些字符串。

最后,在最后一个语句中,我们输出Hello,一个空格,who变量中的字符串以及一个新行(newline)。fmt包中拥有许多不同的打印输 出变体,一些像fmt.Println()的将整齐地输出任何传入的值,其他的诸如fmt.Printf将使用占位符以提供更佳的格式控制。

另外一个例子 – 二维slices

下一个例子bigdigits程序从命令行(以一个字符串形式)读取一个数,并在控制台上使用"大号字体"输出这个数。早在二十世纪,在很多用户 共享一台高速行打印机的场合,按惯例将使用这种技术在每个用户的打印工作之前放置一个封面,该封面可以展示用于识别的细节,诸如用户名以及将被打 印的文件的名字。

我将分三段回顾这个程序的代码:首先是import部分,然后是静态数据,最后是处理过程。不过现在让我们来看看一个样例,了解一下这个程序是如 何工作的吧:

$ ./bigdigits  290175493

每位数字都由一个字符串slice表示,所有数字一起由一个元素为字符串slice的slice表示。在查看数据之前,这里我们展示一下如何声明 和初始化一个一维的字符串和数字slice:

longWeekend := []string{"Friday", "Saturday", "Sunday", "Monday"}
var lowPrimes = []int{2, 3, 5, 7, 11, 13, 17, 19}

slice用[]Type表示,如果我们要初始化它们,我们可以直接在后面跟上一个用大括号包裹、逗号分隔的对应类型的元素列表。我们本可以用同 样的变量声明语法来声明这两个变量,但在这里我们为展示两种语法的差异以及一个稍后即将说明的原因,lowPrimes slice使用了一个更长形式的声明。由于slice类型可以作为slice类型的元素类型,因此我们可以很容易地创建多维集合(slice的slice 等)。

bigdigits程序只需要导入四个包。

import (
    "fmt"
    "log"
    "os"
    "path/filepath"
)

fmt包提供了用于文本格式化和读取格式化文本的函数。log包提供了日志记录函数。os包提供了平台无关的操作系统变量以及函数,其中就包括持 有命令行参数值的[]string类型的os.Args变量。path包下面的filepath包提供跨平台操作文件名和路径的相关函数。注意对 于逻辑上存在包含关系的包(译注:如path/filepath)来说,我们在代码中使用时只指明其名字的最后部分(这里是filepath)。

对于bigdigits这个程序,我们需要一个二维的数据(一个字符串slice的slice)。下面就是创建它的方法,通过代表数字0的字符串 布局我们可以看出这个数字对应的字符串在输出时相应的行。3到8的数字对应的字符串这里省略了。

var bigDigits = [][]string{
   {"  000  ",
    " 0   0 ",
    "0     0",
    "0     0",
    "0     0",
     "0  0",
     " 000 "},
    {" 1 ", "11 ", " 1 ", " 1 ", " 1 ", " 1 ", "111"},
    {"222","2  2","  2","  2 ","2  ","2  ","22222"},
    // … 3 to 8 …
    {" 9999", "9  9", "9  9", " 9999", "  9", "  9", "  9"},

在函数或方法外面声明的变量不可以使用:=操作符,但我们可以使用长声明形式(使用关键字var)以及赋值操作符(=)来得到同样的效果,就如我 们这里为bigDigits程序中的变量所做的那样(前面对lowPrime变量的声明)。我们仍然无需指定bigDigits变量的类型,Go 可以从赋值中推断出其类型。

我们把计算工作留给了Go编译器,因此也没有必要指出slice的维数。Go的一个方便之处就是它对使用了括号的符合字面值的良好支持,这样我们 无需在一个地方声明一个数据变量,然后在另外一个地方用数据给它赋值了。

main()函数读取命令行,并使用这些数据产生输出,这个函数只有20行。

func main() {
    if len(os.Args) == 1 {
        fmt.Printf("usage: %s <whole-number>\n", filepath.Base(os.Args[0]))
        os.Exit(1)
    }
 
    stringOfDigits := os.Args[1]
    for row := range bigDigits[0] {
        line := ""
        for column := range stringOfDigits {
            digit := stringOfDigits[column] – '0'
            if 0 <= digit && digit <= 9 {
                line += bigDigits[digit][row] + " "
            } else {
                log.Fatal("invalid whole number")
            }
        }
        fmt.Println(line)
    }
}

程序一开始检查是否有任何命令行参数。如果没有,len(os.Args)的值将为1(回忆一下,os.Args[0]中存放的是程序名,因此这 个slice的长度至少是1)。如果这个if语句条件成立,我们将使用fmt.Printf输出一条适当程序用法信息,该Printf函数使用类 似C/C++中printf()或Python中%操作符的%占位符。

path/filepath包提供路径操作函数- 比如,filepath.Base()函数返回给定路径的基本名(basename,即文件名)。在输出这条信息后,程序使用os.Exit()函数结束 程序,并返回1给操作系统。在类Unix系统中,一个值为0的返回值用于表示成功,非0值标识用法错误或失败。

filepath.Base()函数的使用向我们说明了Go的一个美妙的特性:当一个包被导入时,无论它是顶层的包还是逻辑上内置于其他包中的包 (例如:path/filepath),我们总是可以只通过其名字的最后部分(即filepath)来引用它。我们还可以给包赋予本地名字以避免 名字冲突。

如果至少传入了一个命令行参数,第一个参数将被拷贝到stringOfDigits变量(string类型)中。要想将用户输入的数字转换成大数 字,我们必须迭代处理bigDigits slice的每一行,即每个数字的第一行(最上面的一行),接下来第二行,依次类推。我们假设所有bigDigits的slice都具有相同数量的行,这 样我们可以从第一个slice那里得到行数。Go的for循环对不同场景有不同的应对语法;在这个例子中,for…range循环返回 slice中每个元素的索引位置信息。

行和列的循环部分的代码可以这样来写:

for row := 0; row < len(bigDigits[0]); row++ {
    line := ""
    for column := 0; column < len(stringOfDigits); column++ {
        …

这是一个C、C++和Java程序员都熟悉的语法形式,在Go中它也是有效的。(与C、C++和Java不同在于,在Go中,++和–操作符只 能用作语句,而不能用作表达式。此外,它们只能被用作后缀操作符,而不能作为前缀操作符。这意味着求值顺序导致的相关问题在Go中不会发生- 谢天谢地,像f(i++)和a[i] = b[++i]这样的表达式在Go中是非法的。) 然而,for…range语法更加短小,也更加方便。

在每次行迭代时,代码会将行的line赋值为空字符串。接下来,我们做迭代处理从用户那里获取的stringOFDigits中的列(即,字 符)。Go的字符串使用UTF-8字符,因此本质上一个字符很可能用两个或更多字节表示。但这不是这里要讨论的话题,因为我们只关心数值0、 1、…、9,这些数值用UTF-8字符表示时只需一个字节,与用7比特ASCII字符表示所使用的字节值相同。

当我们索引字符串中的某个特定位置时,我们获取了那个位置的字节值。(在Go中byte类型是uint8类型的同义词。)因此我们获取到命令行字 符串特定列上的字节值,减去数字0的字节值后,得到它表示的数字。在UTF-8(以及7比特ASCII)中,字符'0'是码点(字符)十进制值 48,字符'1'是码点十进制值49,依次类推。这样举例,如果我们有字符'3'(码点1),我们可以通过做减法'3' – '0'(即51-48)的结果得到其整型值3。

Go使用单引号表示字符字面值,一个字符字面值是一个与Go任何整型类型都兼容的整数。Go的强类型意味着如果不进行显式转型,我们无法将一个 int32类型的数与一个int16类型的数相加,不过,Go中的数值常量和字面值自适应于其上下文,这样一来,这里的'0'被认为是一个字节。

如果这个数字(byte类型)在范围内,我们会将对应的字符串加到line变量中。(在if语句中,常量0和9被认为是byte类型,因为它们是 数值类型,但如果数值是一个不同的类型,比如说,int,它们将会被当作新类型对待。)虽然Go中的字符串是不可改变的,Go仍然支持+=附加操 作符以提供一个便于使用的语法。(它通过在后台替换掉原先的字符串。)Go同样支持+字符串连接操作符,该操作将返回一个由左右字符串操作数连接 而成的新字符串。

为了获取对应的字符串,我们根据这个数值访问bigDigits slice,然后访问其中我们需要的行(字符串)。

如果数值超出了范围(比如,由于stringOfDigits包含了一个非数值),我们调用log.Fatal()函数记录一条错误信息。如果没有显式指 定其他日志输出目标,这个函数会在os.Stderr中记录下日期、时间和错误信息。然后该函数调用os.Exit(1)结束程序。还有 一个名为log.Fatalf()的函数可以做同样的事情,但它接受%占位符。我们没有在第一个if语句中使用log.Fatal()函数,因为 我们想输出程序的使用方法信息,但不要log.Fatal()默认输出的日期和时间信息。

一旦给定行的所有数字的字符串都累加完毕,完整的一行就被输出。在这里,我们输出了7行,因为每个bigDigits slice中的数字由七个字符串表示。

最后一点是声明和定义的顺序无关紧要。因此在bigdigits/bigdigits.go文件中,我们可在main()函数前声明bigDigits变 量,也可在后面声明。在这个例子里,我们将main()函数放在前面,对于这篇文章中的例子,我们通常更倾向于自顶向下的排序。

这里的两个例子已经涵盖了大量特性,但它们所展示的资料与其他主流语言甚为相似,即便语法稍有不同。下周的文章将检视Go语言的其余特性,包含一些高级方面的特性。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系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