上一篇文章介绍了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 testtesting包组成的轻量级测试框架。

首先在$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.Errort.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