上一篇文章介绍了Go代码的组织结构及一些基本概念,下面将以github.com/favorstack
作为基路径,在工作空间中创建一个目录来保存源代码,介绍一下基本的语法。
一 第一个程序
Hello world程序
1). 创建基路径:
$ mkdir -p $GOPATH/src/github.com/favorstack
或者:
$ mkdir -p ~/go/src/github.com/favorstack
2). 新建一个工程目录go-example
:
该目录可以作为git仓库的根目录:
$ cd $GOPATH/src/github.com/favorstack && mkdir go-example
3). 在上述目录下创建hello
目录:
$ cd go-example && mkdir hello
4). 在hello
目录下新建一个hello.go
文件:
$ cd hello && vi hello.go
然后输入以下代码:
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
5). 保存退出。
6). 编译执行
在第一篇文章中我们已经使用过go build
命令了,这次我们直接安装到$GOPATH
目录的bin
下:
执行go install
命令编译并安装:
$ go install
没有任何信息输出则表示编译没问题。
直接执行~/go/bin
下的命令,需要将该目录添加到$PATH
环境变量中,如果你还没有加上,现在是个好机会,可以参考上一篇文章。
没问题的话,我们可以看到可执行程序已经编译好并安装在~/go/bin
目录下了,直接执行hello
命令即可(Windows下是hello.exe
):
$ hello
Hello World!
上面省略了包路径,只能在当前目录下执行。如果加上包路径,则可以在任意位置执行:
$ go install github.com/favorstack/go-example/hello
在不指定包路径时,go install
会从当前目录查找源代码,而指定包路径后,会从$GOPATH
指定的位置查找github.com/favorstack/go-example/hello
包的源代码,所以在指定了包路径后,无需在当前目录下执行安装命令。
另外,源代码文件未改变(MD5值未改变)的情况下,多次执行
go isntall
所编译安装的可执行程序并不会发生改变,实际上,go甚至并未进行新的编译而是直接复制的第一次编译后的结果,你可以对目标文件执行MD5值校验多次编译的结果来验证这一点。
代码解释
1.go源文件后缀必须以.go
结尾:
例如hello.go
2.go代码的第一行有效(非注释)代码总是以包的声明开始:
例如package main
3.包的声明语法:
package 包名
包名
即为在其他模块导入时使用的名称,同一个包下的所有源文件必须使用相同的包名。包名应该尽量简短,简洁,好记,按照约定,所有包名使用小写字母,且使用单个单词表示,不需要使用下划线或大小写混合的方式。
在Go语言中,有用的文档注释通常比超长名称更有价值
4.导入包语法:
import "包导入路径"
多个导入包需要放到括号中:
import (
"包路径1"
"包路径2"
...
)
5.重命名导入:
如果导入的包具有相同的包名时,需要重命名其中一个包名:
import (
"path1/package"
mypackage "path2/package"
)
其中,左侧为新的包名mypackage
,右侧为原始包名"path2/package"
命令
go doc [command]
可以查询指定包的用法, 例如go doc net/http
6.Go语句的结尾
Go语句无需以分号;
结尾,但是如果多条语句放在一行,则需要用分号隔开,我们并不推荐这种多条语句放在一行的用法。
二 第一个库
字符串反转工具
接下来是一个字符串反转的小工具示例。
1). 首先创建包路径:
$ mkdir $GOPATH/src/github.com/favorstack/go-example/stringutil
2). 接下来在上面的目录下创建reverse.go
文件:
$ cd stringutil && vi reverse.go
3). 输入以下内容:
// stringutil包包含字符串处理相关的函数
package stringutil
// Reverse函数返回其参数字符串从左到右的反转形式
func Reverse(s string) string {
r := []rune(s)
length := len(r)
for i, j := 0, length - 1; i < length/2; i, j = i + 1, j - 1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
4). 然后用go build
编译一下:
$ go build github.com/favorstack/go-example/stringutil
或者如果你当前已经在stringutil
目录下了的话,直接执行go build
即可:
# if currently in github.com/favorstack/go-example/stringutil
$ go build
需要注意的是,本次编译并不会像hello.go
一样产生编译后的文件,实际上,它已经把编译好的包放到了本地缓存中。
5). 接下来修改我们的hello.go
程序使用刚才编写的Reverse
函数:
package main
import (
"fmt"
// 导入字符串工具包
"github.com/favorstack/go-example/stringutil"
)
func main() {
fmt.Println("Hello World!")
s := "悠云白雁过南楼"
fmt.Println(s)
// 使用反转函数
fmt.Println(stringutil.Reverse(s))
}
6). 然后保存安装:
$ go install github.com/favorstack/go-example/hello
7). 运行:
$ hello
Hello World!
悠云白雁过南楼
楼南过雁白云悠
现在,工作空间的目录结构应该是这样:
~/go/ # GOPATH
bin/
hello # 可执行文件
src/
github.com/favorstack/go-example/
hello/
hello.go # 可执行文件源文件
stringutil/
reverse.go # 工具包源文件
代码解释
1.包名
需要与导入路径
的最后一个元素
保持一致;
虽然语法上源文件的包名可以和导入路径的最后一个元素不一样,但是按照Go的约定,我们一般习惯将包名
与导入路径
的最后一个元素
保持一致
例如,示例中的导入路径github.com/favorstack/go-example/stringutil
(在hello.go
中),包名对应的为stringutil
(在reverse.go
中);
另外,可执行文件的包总是声明为package main
,Go会查找main
包下的main()
函数作为程序入口。
假如我们将reverse.go
放到github.com/favorstack/go-example/util
下,但是声明依然是package stringutil
,那么,相应的导入路径会变为github.com/favorstack/go-example/util
,但是如果你试图使用util
来调用其中的函数:util.Reverse(...)
,则会报错:
$ go build
# github.com/favorstack/go-example/hello
./hello.go:5:2: imported and not used: "github.com/favorstack/go-example/util" as stringutil
./hello.go:12:14: undefined: util
上述错误包含两点:
1) imported and not used: "github.com/favorstack/go-example/util" as stringutil
提示stringutil
包导入后未曾使用(实际用的是util
)。在Go中,不允许导入的包不使用,即不允许多余的包导入,这样会造成资源浪费,所以Go直接从语言层面上就禁止了这种做法。当然,这一点还有另一种折中的办法来导入不使用的包,参见下面的空白标识符
;
2)undefined: util
提示未定义的包util
(实际定义的是stringutil
)。
因为包的名字实际上还是叫stringutil
,因此,依然需要使用stringutil.Reverse(...)
。所以为了不必要的麻烦和误解,请一定要将包名
与导入路径
的最后一个元素
保持一致
2.变量的声明
在reverse.go
中,有如下两行代码:
r := []rune(s)
length := len(r)
符号:=
在Go中称作简化变量声明运算符
。这个运算符用于声明一个变量,同时给这个变量赋予初始值。编译器使用函数返回值的类型来确定每个变量的类型。简化变量声明运算符只是一种简化记法,让代码可读性更高。
另外,Go还有一种声明变量的方式:
var 变量名 变量类型
例如:
// 声明一个类型为string的变量s
var s string
使用关键字var
声明的变量和简化变量声明运算符:=
声明的变量没有任何区别。一般情况下,要声明初始值为零值的变量,应该使用var关键字声明变量;如果提供确切的非零初始化变量或者使用函数返回值创建变量,应该使用简化变量声明运算符。
3.注释
与大多数语言一样,在Go中,以//
开头的行表示注释
三 测试
第一个测试代码
Go自身带有一个由go test
和testing
包组成的轻量级测试框架。
首先在$GOPATH/src/github.com/favorstack/go-example/stringutil
目录下创建一个reverse_test.go
测试文件,内容如下:
package stringutil
import "testing"
func TestReverse(t *testing.T) {
cases := []struct {
in, want string
}{
{"Hello, world!", "!dlrow ,olleH"},
{"悠云白雁过南楼", "楼南过雁白云悠"},
{"", ""},
}
for _, c := range cases {
got := Reverse(c.in)
if got != c.want {
t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
}
}
}
保存退出,然后在当前目录运行测试:
$ go test
PASS
ok github.com/favorstack/go-example/stringutil 0.007s
或者在任意位置运行测试:
$ go test github.com/favorstack/go-example/stringutil
ok github.com/favorstack/go-example/stringutil 0.007s
代码解释
1.Go测试文件格式
Go测试文件必须以_test.go
结尾,测试函数必须以Test
开头,形如TestXxx
格式,Xxx
必须以大写字母开头,且函数参数签名必须是func (t *testing.T)
形式。测试框架会执行每一个这样的函数,如果某个这样的函数调用了像t.Error
或 t.Fail
这样的失败函数,则该测试没有通过。
2.struct关键字
Go使用struct
关键字声明自定义结构类型。
现在只需要知道,这段代码声明了一个元素为自定义结构类型的切片(cases
变量),该自定义结构类型有两个字符串类型的字段(或者叫属性)in
,want
,并且该切片默认初始化了3个元素。切片也是一种数据结构,有点像动态的数组。切片是可以进行迭代的。
cases := []struct {
in, want string
}{
{"Hello, world!", "!dlrow ,olleH"},
{"悠云白雁过南楼", "楼南过雁白云悠"},
{"", ""},
}
3.for range 循环
语法:
for ... := range ... {
statement
...
}
关键字range
可以用于迭代数组、字符串、切片、映射和通道等数据类型。
与java不同,Go函数允许返回多个值,由于切片数据类型的每个元素都有对应的索引位置,所以上述测试用例中的for range
循环中会返回两个值:_
, c
,其中第一个为当前元素的索引,第二个为当前元素本身。符号_
在Go语言中有特殊含义,称为空白标识符
。
4.空白标识符
下划线字符_
在Go语言里称为空白标识符,这个标识符用来抛弃不想继续使用的值,如给导入的包赋予一个空名字,或者忽略函数返回的不感兴趣的值(本例)。
主要有以下两个作用:
- 1)让Go语言对包做初始化操作,但是并不使用包里的标识符。为了让程序的 可读性更强,Go编译器不允许声明导入某个包却不使用。下划线可以让编译器接受这类导入,并且调用对应包内的所有代码文件里定义的
init
初始化函数。 - 2)占位符,如果要调用的函数返回多个值,而其中的某个值又不需要,就可以使用下划线标识符将其忽略。上述测试用例中,迭代过程中我们并不关心元素的索引,所以使用
_
将其忽略掉了。
四 go get命令
导入路径中如果包含你的远程仓库地址的话,go get
命令可以根据这个导入路径自动从远程仓库获取这个包的源代码。例如,本文示例的代码已托管在GitHub上:github.com/favorstack/go-example,那么你可以直接使用go get
获取helloworld的源代码,并自动构建编译安装:
$ go get github.com/favorstack/go-example/hello
$ hello
Hello World!
悠云白雁过南楼
楼南过雁白云悠
如果你的工作空间中没有这个包,go get
会将这个包拉取下来并放到$GOPATH
指定的目录列表的第一个路径下;如果已经存在,则只是执行一下安装,不会再重新拉取(如果远程代码更新了,go get默认并不会更新最新的代码,如果需要更新,可以指定-u
参数,前提是该包在本地未被修改)。这也是Go代码与他人共享的一种方式。
参考
https://golang.org/doc/code.htmlhttps://golang.org/doc/effective_go.htmlhttps://golang.org/pkg/testing