作者:ffhelicopter(李骁) 时间:2018-04-15
一直都很懒,所以这几年也没写什么东西。这次写《Go语言四十二章经》,纯粹是因为开发过程中碰到过的一些问题,踩到过的一些坑,感觉在Go语言学习使用过程中,有必要深刻理解这门语言的核心思维、清晰掌握语言的细节规范以及反复琢磨标准包代码设计模式,于是才有了这本书。
Go语言以语法简单、门槛低、上手快著称。但入门后很多人发现要写出地道的、遵循 Go语言思维的代码却是不易。
在刚开始学习中,我带着比较强的面向对象编程思维惯性来写代码,但后来我转变了思路,因为我发现,带着面向对象的思路来写Go 语言代码会很难继续写下去,或者说看了系统源代码或其他知名开源包源代码后,围绕着Struct和Interface来写代码会更高效,代码更美观。虽然有人认为,Go语言的Strcut 和 Interface 一起,配合方法,也可以理解为面向对象,这点我姑且认可,但开发中不要过意考虑这些。因为在Go 语言中,Interface接口的使用将更为灵活,刻意追求面向对象,会导致你很难理解接口在Go 语言中的妙处。
作为Go语言的爱好者,在阅读系统源代码或其他知名开源包源代码时,发现大牛对这门语言的了解之深入,代码实现之巧妙优美,所以我建议你有时间多多阅读这些代码。网上有说Go大神的标准是“能理解简洁和可组合性哲学”,的确Go语言追求代码简洁到极致,而组合思想可谓借助于struct和interface两者而成为Go的灵魂。
Function,Method,Interface,Type等名词是程序员们接触比较多的关键字,但在Go语言中,你会发现,其有了更强大,更灵活的用法。当你彻底理解了Go语言相关基本概念,以及对其特点有深入的认知,当然这也这本书的目的,再假以时日多练习和实践,我相信你应该很快就能彻底掌握这门语言,成为一名出色的Gopher。
这本书适合Go语言新手来细细阅读,对于有一定经验的开发人员,也可以根据自己的情况,跳开一些章节来看。最后,希望更多的人了解和使用Go语言,也希望阅读本书的朋友们多多交流。虽然本书中例子都经过实际运行,但难免出现错误和不足之处,烦
作者:李骁
Go语言是一门全新的静态类型开发语言,具有自动垃圾回收,丰富的内置类型, 函数多返回值,错误处理,匿名函数, 并发编程,反射,defer等关键特征,并具有简洁、安全、并行、开源等特性。从语言层面支持并发,可以充分的利用CPU多核,Go语言编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。系统标准库功能完备,尤其是强大的网络库让建立Web服务成为再简单不过的事情。简单易学,内置runtime,支持继承、对象等,开发工具丰富,例如gofmt工具,自动格式化代码,让团队代码风格完美统一。同时Go非常适合用来进行服务器编程,网络编程,包括Web应用、API应用,分布式编程等等。
“Go让我体验到了从未有过的开发效率。”谷歌资深工程师罗布·派克(Rob Pike)如是说道,和C++或C一样,Go是一种系统语言,他表示,“使用它可以进行快速开发,同时它还是一个真正的编译语言,我们之所以现在将其开源,原因是我们认为它已经非常有用和强大。”
Go语言自2009年面世以来,已经有越来越多的公司开始转向Go语言开发,比如腾讯、百度、阿里、京东、小米以及360,而七牛云其技术栈基本上完全采用Go语言来开发。还有像今日头条、UBER这样的公司,他们也使用Go语言对自己的业务进行了彻底的重构。在全球范围内Go语言的使用不断增长,尤其是在云计算领域,用Go语言编写的几个主要云基础项目如Docker和Kubernetes,都取得了巨大成功。除此之外,还有各种有名的项目如etcd/consul/flannel等等,均使用Go语言实现。
Go语言有两快,一是编译运行快,还有一个是学习上手快。Go语言的学习曲线并不陡峭,无论是刚开始接触编程的朋友,还是有其他语言开发经验而打算学习Go语言的朋友,大家都可以放心大胆来学习和了解Go语言,“它值得拥有!”
让我们开始Go语言学习之旅吧!
1.1 Go安装
要用Go语言来进行开发,需要先搭建开发环境。Go 语言支持以下系统:
Linux
FreeBSD
Mac OS
Windows
首先需要下载Go语言安装包,Go语言的安装包下载地址为:https://golang.org/dl/ ,
作者:李骁
在 Go 语言中,数据类型可用于参数和变量声明。
Go 语言按类别有以下几种数据类型:
布尔型:
布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true
。
数字类型:
整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。
字符串类型:
字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本。
派生类型:
包括:
(a) 指针类型(Pointer)
(b) 数组类型
(c) 结构类型(struct)
(d) Channel 类型
(e) 函数类型
(f) 切片类型
(g) 接口类型(interface)
(h) Map 类型
Go 也有基于架构的类型,例如:int、uint 和 uintptr,这些类型的长度都是根据运行程序所在的操作系统类型所决定的。
类型 | 符号 | 长度范围 |
---|---|---|
uint8 | 无符号 | 8位整型 (0 到 255) |
uint16 | 无符号 | 16位整型 (0 到 65535) |
uint32 | 无符号 | 32位整型 (0 到 4294967295) |
uint64 | 无符号 | 64位整型 (0 到 18446744073709551615) |
int8 | 有符号 | 8位整型 (-128 到 127) |
int16 | 有符号 | 16位整型 (-32768 到 32767) |
int32 | 有符号 | 32位整型 (-2147483648 到 2147483647) |
int64 | 有符号 | 64位整型 (-9223372036854775808 到 9223372036854775807) |
主要是为了表示小数,也可细分为float32和float64两种。浮点数能够表示的范围可
作者:李骁
Go 语言变量名由字母、数字、下划线组成,其中首个字母不能为数字。
var (
a int
b bool
str string
)
这种因式分解关键字的写法一般用于声明全局变量,一般在func 外定义。
当一个变量被var声明之后,系统自动赋予它该类型的零值:
记住,这些变量在 Go 中都是经过初始化的。
多变量可以在同一行进行赋值,也称为 并行 或 同时 或 平行赋值。如:
a, b, c = 5, 7, "abc"
简式声明:
a, b, c := 5, 7, "abc" // 注意等号前的冒号
右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 5, b 的值是 7,c 的值是 "abc"。
简式声明一般用在func内,要注意的是:全局变量和简式声明的变量尽量不要同名,否则很容易产生偶然的变量隐藏Accidental Variable Shadowing。
即使对于经验丰富的Go开发者而言,这也是一个非常常见的陷阱。这个坑很容易挖,但又很难发现。
func main() {
x := 1
fmt.Println(x) // prints 1
{
fmt.Println(x) // prints 1
x := 2
fmt.Println(x) // prints 2
}
fmt.Println(x) // prints 1 (不是2)
}
如果你想要交换两个变量的值,则可以简单地使用:
a, b = b, a
(在 Go 语言中,这样省去了使用交换函数的必要)
空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7
中被抛弃。
_, b = 5, 7
_ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但
作者:李骁
常量使用关键字 const 定义,用于存储不会改变的数据。
存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量的定义格式:const identifier [type] = value,例如:
const Pi = 3.14159
在 Go 语言中,你可以省略类型说明符 [type],因为编译器可以根据变量(常量)的值来推断其类型。
显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"
一个没有指定类型的常量被使用时,会根据其使用环境而推断出它所需要具备的类型。换句话说,未定义类型的常量会在必要时刻根据上下文来获得相关类型。
常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
在这个例子中,iota 可以被用作枚举值:
const (
a = iota
b = iota
c = iota
)
第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
const (
a = iota
b
c
)
注意:
const (
a = iota
b = 8
c
)
a, b, c分别为0, 8, 2,可以简单理解为在一个const块中,每换一行定义个常量,iota 都会自动+1
( 关于 iota 的使用涉及到非常复杂多样的情况 ,这里不展开来讲了,有兴趣可以查查资料研究)
iota 也可以用在表达式中,如:iota + 50。在每遇到一个新的常量块或单个常量声明时, iota 都会重置为 0( 简单地讲,每遇到一次 const 关键字,iota 就重置为 0 )。
使用位左移与 iota 计数配合可优雅地实现存储单位的常量枚举:
type ByteSize float64
const (
_ = iota // 通过赋值给空白
作者:李骁
局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
全局变量
作用域都是全局的(在本包范围内)
在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
全局变量可以在任何函数中使用。
简式变量
使用 := 定义的变量,如果新变量p与那个同名已定义变量 (这里就是那个全局变量p)不在一个作用域中时,那么Go 语言会新定义这个变量p,遮盖住全局变量p。刚开始很容易在此犯错而茫然,解决方法是局部变量尽量不同名。
注意,简式变量只能在函数内部声明使用,但是它可能会覆盖函数外全局同名变量。而且你不能在一个单独的声明中重复声明一个变量,但在多变量声明中这是允许的,而且其中至少要有一个新的声明变量。重复变量需要在相同的代码块内,否则你将得到一个隐藏变量。
如果你在代码块中犯了这个错误,将不会出现编译错误,但应用运行结果可能不是你所期望。所以尽可能避免和全局变量同名。
作者:李骁
包通过下面这个被编译器强制执行的规则来决定是否将自身的代码对象暴露给外部文件:
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
(大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)。
因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。
假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:
pack1.Thing //(pack1 在这里是不可以省略的)
因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如 pack1.Thing 和 pack2.Thing。
注意事项:
如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如 imported and not used: os,这正是遵循了 Go 的格言:“没有不必要的代码!”。
干净、可读的代码和简洁性是 Go 追求的主要目标。通过 Gofmt 来强制实现统一的代码风格。Go 语言中对象的命名也应该是简洁且有意义的。像 Java 和 Python 中那样使用混合着大小写和下划线的冗长的名称会严重降低代码的可读性。名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。返回某个对象的函数或方法的名称一般都是使用名词,没有 Get... 之类的字符,如果是用于修改某个对象,则使用 SetName。有必须要的话可以使用大小写混合的方式,如 MixedCaps 或 mixedCaps,而不是使
作者:李骁
包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。
如同其它一些编程语言中的类库或命名空间的概念,每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。
你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main 。
package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。package main包下可以有多个文件,但所有文件中只能有一个main()方法,main()方法代表程序入口。
一个应用程序可以包含不同的包,而且即使你只使用 main 包也不必把所有的代码都写在一个巨大的文件里:你可以用一些较小的文件,并且在每个文件非注释的第一行都使用 package main 来指明这些文件都属于 main 包。如果你打算编译包名不是为 main 的源文件,如 pack1,编译后产生的对象文件将会是 pack1.a 而不是可执行程序。另外要注意的是,所有的包名都应该使用小写字母。当然,main包是不能在其他文档import的,编译器会报错:
import "xx/xx" is a program, not an importable package。
简单地说,在含有mian包的目录下,你可以写多个文件,每个文件非注释的第一行都使用 package main 来指明这些文件都属于这个应用的 main 包,只有一个文件能有mian() 方法,也就是应用程序的入口。main包不是必须的,只有在可执行的应用程序中需要。
一个 Go 程序是通过 import 关键字将一组包链接在一起。
import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。包名被封闭在半角双引号 "" 中。如果你打算从已编译的包中导入并加载公开声明的方法,不需要插入已编译包的源代
作者:李骁
Go的工程项目管理非常简单,使用目录结构和package名来确定工程结构和构建顺序。
环境变量GOPATH在项目管理中非常重要,想要构建一个项目,必须确保项目目录在GOPATH中。而GOPATH可以有多个项目用";"分隔。
Go 项目目录下一般有三个子目录:
我们重点要关注的其实就是src文件夹中的目录结构。
为了进行一个项目,我们会在GoPATH目录下的src目录中,新建立一个项目的主要目录,比如我写的一个WEB项目《使用gin快速搭建WEB站点以及提供RestFull接口》。
https://github.com/ffhelicopter/tmm
项目主要目录“tmm”: $GoPATH/src/github.com/ffhelicopter/tmm
在这个目录(tmm)下面还有其他目录,分别放置了其他代码,大概结构如下:
src/github.com/ffhelicopter/tmm
/api
/handler
/model
/task
/website
main.go
main.go 文件中定义了package main 。同时也在文件中import了
"github.com/ffhelicopter/tmm/api"
"github.com/ffhelicopter/tmm/handler"
2个自定义包。
上面的目录结构是一般项目的目录结构,基本上可以满足单个项目开发的需要。如果需要构建多个项目,可按照类似的结构,分别建立不同项目目录。
当我们运行go install main.
作者:李骁
Go语言的算术运算符:
运算符 | 含义 | 示意 |
---|---|---|
+ | 相加 | A + B |
- | 相减 | A - B |
* | 相乘 | A * B |
/ | 相除 | B / A 结果还是整数 8/3=2 |
% | 求余 | B % A |
++ | 自增 | A++ 1 |
-- | 自减 | A-- |
Go语言的关系运算符:
运算符 | 含义 | 示意 |
---|---|---|
== | 检查两个值是否相等。 | (A == B) 为 False |
!= | 检查两个值是否不相等。 | (A != B) 为 True |
> | 检查左边值是否大于右边值。 | (A > B) 为 False |
< | 检查左边值是否小于右边值。 | (A < B) 为 True |
= |
检查左边值是否大于等于右边值。 | (A >= B) 为 False |
<= | 检查左边值是否小于等于右边值。 | (A <= B) 为 True |
Go语言的逻辑运算符:
运算符 | 操作 | 含义 |
---|---|---|
&& | 逻辑与 | 如果两边的操作数都是 True,则条件 True,否则为 False。 |
|| | 逻辑或 | 如果两边的操作数有一个 True,则条件 True,否则为 False。 |
! | 逻辑非 | 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 |
Go语言的位运算符:
位运算符对整数在内存中的二进制位进行操作。
下表列出了位运算符 &,|,和 ^ 的计算:
位 | 位 | & 与 | | 或 | ^ 异或 |
---|---|---|---|---|
p | q | p & q | p | q | p ^ q |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 |