一、Go语言的由来
Go语言亦叫Golong语言,是由谷歌Goggle公司推出。Go语言的主要开发者有:肯.汤姆逊(Ken Thompson)、罗布.派克(Rob Pike)和罗伯特.格里泽默(Robert Griesemer)。
肯.汤姆逊(Ken Thompson):图灵奖得主,Uinx发明人,B语言作者(C语言前身),还做飞行员,后来被谷歌挖走。
罗布.派克(Rob Pike):Unix团队和Plan 9操作系统计划的成员,与Ken老爷子共事多年,并共创出广泛使用的UTF-8 字元编码。
罗伯特.格里泽默(Robert Griesemer):曾协助制作Java的HotSpot编译器,和Chrome浏览器的JavaScript引擎V8。
二、开发Go语言的初衷
根据Go语言开发者自述,近10多年,从单机时代的C语言到现在互联网时代的Java,都没有令人满意的开发语言,而 C++往往给人的感觉是,花了100%的经历,却只有60%的开发效率,产出比太低,Java和C#的哲学又来源于C++。并且,随着硬件的不断升级,这些语言不能充分的利用硬件及CPU。因此,一门高效、简洁、开源的语言诞生了。
Go语言有以下特性:
1.自动垃圾回收
C/C++最头疼的就是指针问题,一不小心就野指针了或者又越界了。在Go语言里再也不用担心,也不用考虑delete或者free,系统自动会回收。
2.函数可以返回多个值
这个很神奇,大多数语言只能返回一个值,Go语言可以返回多个值。这个功能使得开发者再不用绞尽脑汁的想到底怎么返回值的设计,也不用为了传值专门定义一个结构体。
3.并发编程
Go语言天然并发,只需要关键字“go”就可以让函数并发执行,使得并发编程变得更为简单,这也是Go语言最大的优势。
go的目录分为 GOROOT、GOPATH、GOLIB、GOBIN
GOROOT
其实就是golang 的安装路径
当你安装好golang之后其实这个就已经有了
GOLIB
指定公共类库的地方(可以不污染我们的代码)
里边分为 3 层目录 bin、pkg、src
bin 目录下 使用go install 编译后会将二进制文件安装到这个目录
pkg会放一些归档文件,pkg目录会将一些编译出来的文件放到这个目录比如说一些编译的中间文件静态库之类的东西。不过对于初学者来说不需要去管这个目录,只要知道有这么个目录就行了,不需要多做理会,等熟悉了再回过头看看这个目录。
src就厉害了,作为我们开发来说,有百分之九十八的时间都停留在这个目录里。因为它是我们 的源代码存放目录。
GOPATH
GOPATH 的值可以是一个目录的路径,也可以包含多个目录路径,每个目录都代表 Go 语言的一个工作区(workspace),执行go get 的时候会默认下载到第一个目录(%GOLIB%)
GOBIN
go install编译存放路径。不允许设置多个路径。可以为空。为空时则遵循“约定优于配置”原则,可执行文件放在各自GOPATH目录的bin文件夹中(前提是:package main的main函数文件不能直接放到GOPATH的src下面。
go build
如果是普通包,就像pkg下边的 .a文件,当你执行go build之后,它不会产生任何文件。如果你需要在$GOPATH/pkg下生成相应的文件,那就得执行go install
如果是main包,当你执行go build之后,它就会在当前目录下生成一个可执行文件。如果你需要在$GOPATH/bin下生成相应的文件,需要执行go install,或者使用go build -o 路径/a.exe。
如果某个项目文件夹下有多个文件,而你只想编译某个文件,就可在go build之后加上文件名,例如go build a.go;go build命令默认会编译当前目录下的所有go文件。
你也可以指定编译输出的文件名。例如 ns-api 应用,我们可以指定go build -o ns-api.exe,默认情况是你的package名(非main包),或者是第一个源文件的文件名(main包)。
参数的介绍
-o 指定输出的文件名,可以带上路径,例如 go build -o a/b/c
-i 安装相应的包,编译+go install
-a 更新全部已经是最新的包的,但是对标准包不适用
-n 把需要执行的编译命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行
-p n 指定可以并行可运行的编译数目,默认是CPU数目
-race 开启编译的时候自动检测数据竞争的情况,目前只支持64位的机器
-v 打印出来我们正在编译的包名
-work 打印出来编译时候的临时文件夹名称,并且如果已经存在的话就不要删除
-x 打印出来执行的命令,其实就是和-n的结果类似,只是这个会执行
-ccflags ‘arg list’ 传递参数给5c, 6c, 8c 调用
-compiler name 指定相应的编译器,gccgo还是gc
-gccgoflags ‘arg list’ 传递参数给gccgo编译连接调用
-gcflags ‘arg list’ 传递参数给5g, 6g, 8g 调用
-installsuffix suffix 为了和默认的安装包区别开来,采用这个前缀来重新安装那些依赖的包 -race的时候默认已经是-installsuffix race,大家可以通过-n命令来验证
-ldflags ‘flag list’ 传递参数给5l, 6l, 8l 调用
-tags ‘tag list’ 设置在编译的时候可以适配的那些tag
go clean
参数介绍
-i 清除关联的安装的包和可运行文件,也就是通过go install安装的文件
-n 把需要执行的清除命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行的
-r 循环的清除在import中引入的包
-x 打印出来执行的详细命令,其实就是-n打印的执行版本
go fmt
go工具集中提供了一个go fmt命令 它可以帮你格式化你写好的代码文件,使你写代码的时候不需要关心格式,你只需要在写完之后执行go fmt <文件名>.go,你的代码就被修改成了标准格式,但是我平常很少用到这个命令,因为开发工具里面一般都带了保存时候自动格式化功能,这个功能其实在底层就是调用了go fmt。接下来的一节我将讲述两个工具,这两个工具都自带了保存文件时自动化go fmt功能。
参数介绍
-l 显示那些需要格式化的文件
-w 把改写后的内容直接写入到文件中,而不是作为结果打印到标准输出。
-r 添加形如“a[b:len(a)] -> a[b:]”的重写规则,方便我们做批量替换
-s 简化文件中的代码
-d 显示格式化前后的diff而不是写入文件,默认是false
-e 打印所有的语法错误到标准输出。如果不使用此标记,则只会打印不同行的前10个错误。
-cpuprofile 支持调试模式,写入相应的cpufile到指定的文件
go get
这个命令是用来动态获取远程代码包的,目前支持的有BitBucket、GitHub、Google Code和Launchpad。
这个命令在内部实际上分成了两步操作:
第一步是下载源码包
第二步是执行go install
参数介绍
-d 只下载不安装
-f 只有在你包含了-u参数的时候才有效,不让-u去验证import中的每一个都已经获取了,这对于本地fork的包特别有用
-fix 在获取源码之后先运行fix,然后再去做其他的事情
-t 同时也下载需要为运行测试所需要的包
-u 强制使用网络去更新包和它的依赖包
-v 显示执行的命令
go install
这个命令在内部实际上分成了两步操作:第一步是生成结果文件(可执行文件或者.a包),第二步会把编译好的结果移到 GOPATH/pkg 或者 GOPATH/bin。
参数支持go build的编译参数。大家只要记住一个参数-v就好了,这个随时随地的可以查看底层的执行信息。
go test
执行这个命令,会自动读取源码目录下面名为*_test.go的文件,生成并运行测试用的可执行文件。输出的信息类似
参数介绍
-bench regexp 执行相应的benchmarks,例如 -bench=.
-cover 开启测试覆盖率
-run regexp 只运行regexp匹配的函数,例如 -run=Array 那么就执行包含有Array开头的函数
-v 显示测试的详细命令
go tool
go tool下面下载聚集了很多命令,这里我们只介绍两个,fix和vet
go tool fix . 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1,例如API的变化
go tool vet directory|files 用来分析当前目录的代码是否都是正确的代码,例如是不是调用fmt.Printf里面的参数不正确,例如函数里面提前return了然后出现了无用代码之类的。
go generate
这个命令是从Go1.4开始才设计的,用于在编译前自动化生成某类代码。go generate和go build是完全不一样的命令,通过分析源码中特殊的注释,然后执行相应的命令。这些命令都是很明确的,没有任何的依赖在里面。而且大家在用这个之前心里面一定要有一个理念,这个go generate是给你用的,不是给使用你这个包的人用的,是方便你来生成一些代码的。
这里我们来举一个简单的例子,例如我们经常会使用yacc来生成代码,那么我们常用这样的命令:
go tool yacc -o gopher.go -p parser gopher.y
-o 指定了输出的文件名, -p指定了package的名称,这是一个单独的命令,如果我们想让go generate来触发这个命令,那么就可以在当然目录的任意一个xxx.go文件里面的任意位置增加一行如下的注释:
//go:generate go tool yacc -o gopher.go -p parser gopher.y
这里我们注意了,//go:generate是没有任何空格的,这其实就是一个固定的格式,在扫描源码文件的时候就是根据这个来判断的。
所以我们可以通过如下的命令来生成,编译,测试。如果gopher.y文件有修改,那么就重新执行go generate重新生成文件就好。
$ go generate
$ go build
$ go test
godoc
在Go1.2版本之前还支持go doc命令,但是之后全部移到了godoc这个命令下,需要这样安装go get golang.org/x/tools/cmd/godoc
很多人说go不需要任何的第三方文档,例如chm手册之类的,因为它内部就有一个很强大的文档工具。
如何查看相应package的文档呢?
例如builtin包,那么执行godoc builtin
如果是http包,那么执行godoc net/http
查看某一个包里面的函数,那么执行godoc fmt Printf
也可以查看相应的代码,执行godoc -src fmt Printf
通过命令在命令行执行 godoc -http=:端口号 比如godoc -http=:8080。然后在浏览器中打开127.0.0.1:8080,你将会看到一个golang.org的本地copy版本,通过它你可以查询pkg文档等其它内容。如果你设置了GOPATH,在pkg分类下,不但会列出标准包的文档,还会列出你本地GOPATH中所有项目的相关文档,这对于经常被墙的用户来说是一个不错的选择。
其它命令
go还提供了其它很多的工具,例如下面的这些工具
go version 查看go当前的版本
go env 查看当前go的环境变量
go list 列出当前全部安装的package
go run 编译并运行Go程序
var v = 1
ptr := &v
fmt.Println(ptr)
fmt.Println(*ptr)
其中 v 代表被取地址的变量,被取地址的 v 使用 ptr 变量进行接收,ptr 的类型就为*T,称做 T 的指针类型。*代表指针。
在对普通变量使用&操作符取地址获得这个变量的指针后,可以对指针使用 * 操作,也就是指针取值
使用指针同样可以进行数值交换,代码如下
package main import “fmt” // 交换函数 func swap(a, b *int) {
// 取a指针的值, 赋给临时变量t
t := *a
// 取b指针的值, 赋给a指针指向的变量
*a = *b
// 将a指针的值赋给b指针指向的变量
*b = t } func main() { // 准备两个变量, 赋值1和2
x, y := 1, 2
// 交换变量值
swap(&x, &y)
// 输出变量值
fmt.Println(x, y) }
运行结果:
2 1
再看一段代码
package main
import “fmt”func swap(a, b *int) {
b, a = a, b
}func main() {
x, y := 1, 2
swap(&x, &y)
fmt.Println(x, y)
}
运行结果:
1 2
结果表明,交换是不成功的。上面代码中的 swap() 函数交换的是 a 和 b 的地址,在交换完毕后,a 和 b 的变量值确实被交换。但和 a、b 关联的两个变量并没有实际关联。这就像写有两座房子的卡片放在桌上一字摊开,交换两座房子的卡片后并不会对两座房子有任何影响。
new() 函数也可以创建指针
str := new(string)
*str = “ninja”
fmt.Println(*str)
使用指针变量获取命令行的输入信息
Go 语言的 flag 包中,定义的指令以指针类型返回。通过学习 flag 包,可以深入了解指针变量在设计上的方便之处。
下面的代码通过提前定义一些命令行指令和对应变量,在运行时,输入对应参数的命令行参数后,经过 flag 包的解析后即可通过定义的变量获取命令行的数据。
package main
// 导入系统包
import (
“flag”
“fmt”
)
// 定义命令行参数
var mode = flag.String(“mode”, “”, “process mode”)
func main() {
// 解析命令行参数
flag.Parse()
// 输出命令行参数
fmt.Println(*mode)
}
然后使用如下命令行运行:
$ go run flagparse.go --mode=fast
命令行输出结果如下:
fast
由于之前使用 flag.String 已经注册了一个 mode 的命令行参数,flag 底层知道怎么解析命令行,并且将值赋给 mode*string 指针。在 Parse 调用完毕后,无须从 flag 获取值,而是通过自己注册的 mode 这个指针,获取到最终的值。代码运行流程如下图所示。
关键字 func
Go 语言的函数特点
举例
package main
import “fmt”
func main() {
num := 1
data, testData := test(num)
fmt.Println(data, testData)
}
func test(p int) (data int, testData int) {
testData = 2
data = p + 1
return
}
在 Go 语言中,函数也是一种类型,可以和其他类型一样被保存在变量中。下面的代码定义了一个函数变量 f,并将一个函数名 fire() 赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数,代码如下:
package main
import (
“fmt”
)
func fire() {
fmt.Println(“fire”)
}
func main() {
var f func()
f = fire
f()
}
代码输出结果:
fire
字符串的链式处理
代码介绍
package main
import (
“fmt”
“strings”
)
// 字符串处理函数,传入字符串切片和处理链
func StringProccess(list []string, chain []func(string) string) {
// 遍历每一个字符串
for index, str := range list {
// 第一个需要处理的字符串
result := str
// 遍历每一个处理链
for _, proc := range chain {
// 输入一个字符串进行处理,返回数据作为下一个处理链的输入。
result = proc(result)
}
// 将结果放回切片
list[index] = result
}
}
// 自定义的移除前缀的处理函数
func removePrefix(str string) string {
return strings.TrimPrefix(str, “go”)
}
func main() {
// 待处理的字符串列表
list := []string{
“go scanner”,
“go parser”,
“go compiler”,
“go printer”,
“go formater”,
}
// 处理函数链
chain := []func(string) string{
removePrefix,
strings.TrimSpace,
strings.ToUpper,
}
// 处理字符串
StringProccess(list, chain)
// 输出处理好的字符串
for _, str := range list {
fmt.Println(str)
}
}
其他类型能够实现接口,函数也可以,将分别对比结构体与函数实现接口的过程
package main
import (
“fmt”
)
// 调用器接口
type Invoker interface {
// 需要实现一个Call方法
Call(interface{})
}
// 结构体类型
type Struct struct {
}
// 实现Invoker的Call
func (s *Struct) Call(p interface{}) {
fmt.Println(“from struct”, p)
}
// 函数定义为类型
type FuncCaller func(interface{})
// 实现Invoker的Call
func (f FuncCaller) Call(p interface{}) {
// 调用f函数本体
f§
}
func main() {
// 声明接口变量
var invoker Invoker
// 实例化结构体
s := new(Struct)
// 将实例化的结构体赋值到接口7/
invoker = s
// 使用接口调用实例化结构体的方法Struct.Call
invoker.Call(“hello”)
// 将匿名函数转为FuncCaller类型,再赋值给接口
invoker = FuncCaller(func(v interface{}) {
fmt.Println(“from function”, v)
})
// 使用接口调用FuncCaller.Call,内部会调用函数本体
invoker.Call(“hello”)
}
变参函数
package main
import (
"bytes"
"fmt"
)
func printTypeValue(slist ...interface{}) string {
// 字节缓冲作为快速字符串连接
var b bytes.Buffer
// 遍历参数
for _, s := range slist {
// 将interface{}类型格式化为字符串
str := fmt.Sprintf("%v", s)
// 类型的字符串描述
var typeString string
// 对s进行类型断言
switch s.(type) {
case bool: // 当s为布尔类型时
typeString = "bool"
case string: // 当s为字符串类型时
typeString = "string"
case int: // 当s为整型类型时
typeString = "int"
}
// 写值字符串前缀
b.WriteString("value: ")
// 写入值
b.WriteString(str)
// 写类型前缀
b.WriteString(" type: ")
// 写类型字符串
b.WriteString(typeString)
// 写入换行符
b.WriteString("\n")
}
return b.String()
}
func main() {
// 将不同类型的变量通过printTypeValue()打印出来
fmt.Println(printTypeValue(100, "str", true))
}
go的可变参数 要用 … 接收 (slist …interface{})未知类型 用 interface{}
Go语言defer(延迟执行语句)
Go 语言的 defer 语句会将其后面跟随的语句进行延迟处理。在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
多个延迟执行语句的处理顺序
下面的代码是将一系列的数值打印语句按顺序延迟处理,参见演示代码:
package main
import (
"fmt"
)
func main() {
fmt.Println("defer begin")
// 将defer放入延迟调用栈
defer fmt.Println(1)
defer fmt.Println(2)
// 最后一个放入, 位于栈顶, 最先调用
defer fmt.Println(3)
fmt.Println("defer end")
}
代码输出如下:
defer begin
defer end
3
2
1
结果分析如下:
代码的延迟顺序与最终的执行顺序是反向的。
延迟调用是在 defer 所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。