1. go简述
1.1 为什么要创造一门编程语言
- C/C++ 的发展速度无法跟上计算机发展的脚步,十多年来也没有出现一门与时代相符的主流系统编程语言,因此人们需要一门新的系统编程语言来弥补这个空缺,尤其是在计算机信息时代。
- 相比计算机性能的提升,软件开发领域不被认为发展得足够快或者比硬件发展得更加成功(有许多项目均以失败告终),同时应用程序的体积始终在不断地扩大,这就迫切地需要一门具备更高层次概念的低级语言来突破现状。
- 在 Go 语言出现之前,开发者们总是面临非常艰难的抉择,究竟是使用执行速度快但是编译速度并不理想的语言(如:C++),还是使用编译速度较快但执行效率不佳的语言(如:.NET、Java),或者说开发难度较低但执行速度一般的动态语言呢?显然,Go 语言在这 3 个条件之间做到了最佳的平衡:快速编译,高效执行,易于开发。
1.2 Go语言的发展目标
- Go 语言的主要目标是将静态语言的安全性和高效性(C++)与动态语言的易开发性(python)进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行。
因此,Go 语言是一门类型安全和内存安全的编程语言。虽然 Go 语言中仍有指针的存在,但并不允许进行指针运算。
- Go 语言的另一个目标是对于网络通信、并发和并行编程的极佳支持,从而更好地利用大量的分布式和多核的计算机,这一点对于谷歌内部的使用来说就非常重要了。设计者通过** goroutine** 这种轻量级线程的概念来实现这个目标,然后通过** channel **来实现各个 goroutine 之间的通信。他们实现了分段栈增长和 goroutine 在线程基础上多路复用技术的自动化。
这个特性显然是 Go 语言最强有力的部分,不仅支持了日益重要的多核与多处理器计算机,也弥补了现存编程语言在这方面所存在的不足。
- Go 语言中另一个非常重要的特性就是它的构建速度(编译和链接到机器代码的速度),一般情况下构建一个程序的时间只需要数百毫秒到几秒。作为大量使用
C++
来构建基础设施的谷歌来说,无疑从根本上摆脱了 C++ 在构建速度上非常不理想的噩梦。这不仅极大地提升了开发者的生产力,同时也使得软件开发过程中的代码测试环节更加紧凑,而不必浪费大量的时间在等待程序的构建上。
依赖管理是现今软件开发的一个重要组成部分,但是 C 语言中“头文件”的概念却导致越来越多因为依赖关系而使得构建一个大型的项目需要长达几个小时的时间。人们越来越需要一门具有严格的、简洁的依赖关系分析系统从而能够快速编译的编程语言。这正是 Go 语言采用包模型的根本原因,这个模型通过严格的依赖关系检查机制来加快程序构建的速度,提供了非常好的可量测性。
整个 Go 语言标准库的编译时间一般都在** 20 **秒以内,其它的常规项目也只需要半秒钟的时间来完成编译工作。这种闪电般的编译速度甚至比编译 C 语言或者 Fortran 更加快,使得编译这一环节不再成为在软件开发中困扰开发人员的问题。在这之前,动态语言将快速编译作为自身的一大亮点,像C++
那样的静态语言一般都有非常漫长的编译和链接工作。而同样作为静态语言的 Go 语言,通过自身优良的构建机制,成功地去除了这个弊端,使得程序的构建过程变得微不足道,拥有了像脚本语言和动态语言那样的高效开发的能力。
另外,Go 语言在执行速度方面也可以与 C/C++ 相提并论。
由于内存问题(通常称为内存泄漏)长期以来一直伴随着 C++ 的开发者们,Go 语言的设计者们认为内存管理不应该是开发人员所需要考虑的问题。因此尽管 Go 语言像其它静态语言一样执行本地代码,但它依旧运行在某种意义上的虚拟机,以此来实现高效快速的垃圾回收(使用了一个简单的标记-清除算法)。
尽管垃圾回收并不容易实现,但考虑这将是未来并发应用程序发展的一个重要组成部分,Go 语言的设计者们还是完成了这项艰难的任务。
Go 语言还能够在运行时进行反射相关的操作。
使用 go install 能够很轻松地对第三方包进行部署。
此外,Go 语言还支持调用由 C 语言编写的海量库文件(第 3.9 节),从而能够将过去开发的软件进行快速迁移。
1.3 语言的特性
Go 语言从本质上(程序和结构方面)来实现并发编程。
因为 Go 语言没有类和继承的概念,所以它和 Java 或 C++ 看起来并不相同。但是它通过接口 (interface) 的概念来实现多态性。Go 语言有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。因此可以说这是一门混合型的语言。
Go 语言使用静态类型,所以它是类型安全的一门语言,加上通过构建到本地代码,程序的执行速度也非常快。
作为强类型语言,隐式的类型转换是不被允许的,记住一条原则:让所有的东西都是显式的。
Go 语言其实也有一些动态语言的特性(通过关键字 var),所以它对那些逃离 Java 和 .Net 世界而使用 Python、Ruby、PHP 和 JavaScript 的开发者们也具有很大的吸引力。
Go 语言支持交叉编译,比如说你可以在运行 Linux 系统的计算机上开发运行 Windows 下运行的应用程序。这是第一门完全支持 UTF-8 的编程语言,这不仅体现在它可以处理使用 UTF-8 编码的字符串,就连它的源码文件格式都是使用的 UTF-8 编码。Go 语言做到了真正的国际化!
1.4 相关特性的缺失
许多能够在大多数面向对象语言中使用的特性 Go 语言都没有支持,但其中的一部分可能会在未来被支持。
- 为了简化设计,不支持函数重载和操作符重载
- 为了避免在 C/C++ 开发中的一些 Bug 和混乱,不支持隐式转换
- Go 语言通过另一种途径实现面向对象设计(第 10-11 章)来放弃类和类型的继承
- 尽管在接口的使用方面(第 11 章)可以实现类似变体类型的功能,但本身不支持变体类型
- 不支持动态加载代码
- 不支持动态链接库
- 不支持泛型
- 通过 recover() 和 panic() 来替代异常机制(第 13.2-13.3 节)
- 不支持静态变量
1.5 Linux上安装go
- 首先进入go官网下载指定的已编译好的源码包(ps:这里我的电脑是x86-64架构所以选择了`go1.22.1.linux-amd64.tar.gz,将其下载下来
1
wget https://golang.google.cn/dl/go1.22.1.linux-amd64.tar.gz
- 解压缩到指定路径
1
tar -C /usr/lib/go/ -xvf go1.22.1.linux-amd64.tar.gz
建立软连接(可选)
1
ln -s /usr/lib/go/bin/go /usr/bin/go
之后我们需要能够让Linux能够找到
go
这个编译器,所以需要配置~/.bashrc
文件1
2
3export GOROOT=/usr/lib/go
export GOPATH=$PATH:$GOROOT/bin
export PATH=$PATH:/usr/bin
2 Go项目搭建先识
2.1 Go的25个关键字和36个预定义标识符
Go语言的关键字保留很少,只有25个,之所以刻意地将 Go 代码中的关键字保持的这么少,是为了简化在编译过程第一步中的代码解:
break | default | func | interface | select |
case | defer | go | map | struct |
chan | else | goto | package | switch |
const | fallthrough | if | range | type |
continue | for | import | return | var |
除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数(第 6.5 节),它们的作用都将在接下来的章节中进行进一步地讲解。
append | bool | byte | cap | close | complex | complex64 | complex128 | uint16 |
copy | false | float32 | float64 | imag | int | int8 | int16 | uint32 |
int32 | int64 | iota | len | make | new | nil | panic | uint64 |
println | real | recover | string | true | uint | uint8 | uintptr |
2.2 包的概念、导入与可见性
包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。
一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。必须源文件中非注释的第一行指明这个文件属于哪个包: 1
package main
如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。
属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,项目的每个目录都建议只包含一个包。
如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。
Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 .o 的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。
如果 A.go
依赖 B.go
,而B.go
又依赖 C.go
:
- 编译 C.go, B.go, 然后是 A.go.
- 为了编译 A.go, 编译器读取的是 B.o 而不是 C.o.
这种机制对于编译大型的项目时可以显著地提升编译速度。每一段代码只会被编译一次
2.2.1 包的导入
包的导入使用import
,下面使用因式分解关键字导入包 1
2
3
4import (
"fmt"
"os"
)
2.2.2 可见性规则
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public
);标识符如果以小写字母开头,则对包外是不可见的,但是它们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。
假设在包 pack1
中我们有一个变量或函数叫做Thing
(以 T 开头,所以它能够被导出),那么在当前包中导入pack1
包,Thing
就可以像面向对象语言那样使用点标记来调用:pack1.Thing
(pack1 在这里是不可以省略的)。 1
2
3import pack1
pack1.Thing()
如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如
imported and not used: xx
,这正是遵循了 Go 的格言:“没有不必要的代码!”。
2.3 函数
这是定义一个函数最简单的格式:
你可以在括号 () 中写入 0 个或多个函数的参数(使用逗号 , 分隔),每个参数的名称后面必须紧跟着该参数的类型。1
func functionName()
main()
函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。如果你的 main 包的源代码没有包含 main() 函数,则会引发构建错误undefined: main.main
go中的main() 函数既没有参数,也没有返回类型(与 C 家族中的其它语言恰好相反)。如果你不小心为 main() 函数添加了参数或者返回类型,将会引发构建错误:
1
func main must have no arguments and no return values results.
go的函数体必须使用大括号 {} 括起来。,而且左大括号
{
必须与方法的声明放在同一行,这是编译器的强制规定,否则你在使用 gofmt 时就会出现错误提示:>(这是因为编译器会产生 func main() ; 这样的结果,很明显这是错误的) > >究其原因是因为:Go 语言虽然看起来不使用分号作为语句的结束,但实际上这一过程是由编译器自动完成,因此才会引发像上面这样的错误1
build-error: syntax error: unexpected semicolon or newline before {
- 符合规范的函数一般写成如下的形式:
1
2
3func functionName(parameter_list) (return_value_list) {
…
}parameter_list
的形式为(param1 type1, param2 type2, …)
return_value_list
的形式为(ret1 type1, ret2 type2, …)
一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 () 将它们括起来,如:
2.4 注释
go中的注释沿用了c/c++
的注释风格,并且在此基础上提供了一个命令godoc
,该命令从** Go 程序和包文件中提取顶级声明的首行注释以及每个对象的相关注释,并生成相关文档。**
一般用法
go doc package
获取包的文档注释,例如:go doc fmt 会显示使用 godoc 生成的 fmt 包的文档注释。go doc package/subpackage
获取子包的文档注释,例如:go doc container/list。go doc package function
获取某个函数在某个包中的文档注释,例如:go doc fmt Printf 会显示有关 fmt.Printf() 的使用说明。
这个工具只能获取在 Go 安装目录下 ../go/src 中的注释内容。此外,它还可以作为一个本地文档浏览 web 服务器。在命令行输入 godoc -http=:6060,然后使用浏览器打开 http://localhost:6060 后,你就可以看到本地文档浏览服务器提供的页面。
2.5 类型
使用 var
声明的变量的值会自动初始化为该类型的零值。类型定义了某个变量的值的集合与可对其进行操作的集合。
- 类型可以是基本类型,如:int、float、bool、string;
- 结构化的(复合的),如:struct、array、切片 (slice)、map、通道 (channel);
- 只描述类型的行为的,如:interface。
结构化的类型没有真正的值,它使用 nil
作为默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是 NULL 或 0)。值得注意的是,Go 语言中不存在类型继承。
函数也可以是一个确定的类型,就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后 1
2//返回一个typeFunc类型的函数类型
func FunctionName (a typea, b typeb) typeFuncret
返回: 1
return var
使用 type 关键字可以定义你自己的类型,你可能想要定义一个结构体(第 10 章),但是也可以定义一个已经存在的类型的别名,如: 1
type IZ int
1
2
3
4
5type (
IZ int
FZ float64
STR string
)
2.6 Go 程序的一般结构
go的编写结构:
- 在完成包的
import
之后,开始对常量、变量和类型的定义或声明。 - 如果存在
init()
函数的话,则对该函数进行定义(这是一个特殊的函数,每个含有该函数的包都会首先执行这个函数)。 - 如果当前包是
main
包,则定义main()
函数。 - 然后定义其余的函数,首先是类型的方法,接着是按照
main()
函数中先后调用的顺序来定义相关函数,如果有很多函数,则可以按照字母顺序来进行排序。
Go 程序的执行(程序启动)顺序如下:
- 按顺序导入所有被 main 包引用的其它包,然后在每个包中执行如下流程:
- 如果该包又导入了其它的包,则从第一步开始递归执行,但是每个包只会被导入一次。
- 然后以相反的顺序在每个包中初始化常量和变量,如果该包含有 init() 函数的话,则调用该函数。
- 在完成这一切之后,main 也执行同样的过程,最后调用 main() 函数开始执行程序。
2.7 类型转换
在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于 Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样(类型在这里的作用可以看作是一种函数): 1
valueOfTypeB = typeB(valueOfTypeA)
2.8 常量
常量使用关键字 const 定义,用于存储不会改变的数据。存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。常量的定义格式:const identifier [type] = value
,例如: 1
const Pi = 3.14159
- 显式类型定义: const b string = "abc"
- 隐式类型定义: const b = "abc"
但是这种隐式定义必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得,否则就会出错 1
2
3
4正确的做法:
const c1 = 2/3
错误的做法:
const c2 = getNumber() // 引发构建错误: getNumber() used as value
- 常量可以并行赋值
1
2
3
4
5
6const beef, two, c = "eat", 2, "veg"
const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6
const (
Monday, Tuesday, Wednesday = 1, 2, 3
Thursday, Friday, Saturday = 4, 5, 6
) - 常量还可以用作枚举:
1
2
3
4
5const (
Unknown = 0
Female = 1
Male = 2
)
2.9 变量
声明变量的一般形式是使用var
关键字:var identifier type
Go 和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。Go 为什么要选择这么做呢?
首先,它是为了避免像 C 语言中那样含糊不清的声明形式,例如:
int* a, b;
。在这个例子中,只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写。而在 Go 中,则可以很轻松地将它们都声明为指针类型1
var a, b *int
其次,这种语法能够按照从左至右的顺序阅读,使得代码更加容易理解。
1
2
3var a int
var b bool
var str string
Go 编译器的智商已经高到可以根据变量的值来自动推断其类型,这有点像 Ruby 和 Python 这类动态语言,只不过它们是在运行时进行推断,而 Go 是在编译时就已经完成推断过程。因此,你还可以省略type使用下面的这些形式来声明及初始化变量: 1
2
3var a = 15
var b = false
var str = "Go says hello to the world!"
2.9.1 值类型和引用类型
在go中的引用类型不同于C++
中的引用定义,在 Go 语言中,指针属于引用类型,其它的引用类型还包括 slices(第 7 章),maps(第 8 章)和 channel(第 13 章)。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。。另一方面:
- 像值类型的变量,go将它们存储在栈中
- 对于引用类型,其存储在堆中,以便进行垃圾回收
2.9.2 :=初始化声明操作符
我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,因此有什么我们对一个变量直接做初始化,此时var
关键句就显得多余了。所以当我么声明初始化一个变量的时候可以使用:=
初始化声明操作符 1
a,b:=50,false
:=
不允许对以及声明过的变量使用,换一句话就是说:=
只允许声明事初始化 - :=
只允许对局部变量使用,不允许对全局变量进行声明赋值
2.10 init()函数
变量除了可以在全局声明中初始化,也可以在 init() 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main() 函数高。
每个源文件可以包含多个 init() 函数,同一个源文件中的 init() 函数会按照从上到下的顺序执行,如果一个包有多个源文件包含 init() 函数的话,则官方鼓励但不保证以文件名的顺序调用。初始化总是以单线程并且按照包的依赖关系顺序执行。
一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。
3. 基本类型和运算符
go中的基本类型与C++差不多,都有布尔型bool、数字型(int和float32/64)和字符型(byte)。Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。
Go 也有基于架构的类型,例如:int、uint 和 uintptr。这些类型的长度都是根据运行程序所在的操作系统类型所决定的:
- int 和 uint 在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)。
- uintptr 的长度被设定为足够存放一个指针即可。
Go 语言中没有 float 类型。(Go语言中只有 float32 和 float64)没有 double 类型。
3.1 复数类型
这里值得一提的是go引入的复数complex
。Go 拥有以下复数类型 1
2complex64 (32 位实数和虚数)
complex128 (64 位实数和虚数)1
2
3
4fvar c1 complex64 = 5 + 10i
//在使用格式化说明符时,可以使用 %v 来表示复数,但当你希望只表示其中的一个部分的时候需要使用 %f
fmt.Printf("The value is: %v", c1)
// 输出: 5 + 10i1
c = complex(re, im)
real(c)
和 imag(c)
可以分别获得相应的实数和虚数部分。
复数支持和其它数字类型一样的运算。当你使用等号 == 或者不等号 != 对复数进行比较运算时,注意对精确度的把握。cmath 包中包含了一些操作复数的公共方法。如果你对内存的要求不是特别高,最好使用 complex128 作为计算类型,因为相关函数都使用这个类型的参数。
3.2 随机数
一些像游戏或者统计学类的应用需要用到随机数。math/rand
包实现了伪随机数的生成。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for i := 0; i < 10; i++ {
a := rand.Int() //随机生成Int范围随机数
fmt.Printf("%d / ", a)
}
for i := 0; i < 5; i++ {
r := rand.Intn(8) //随机生成[0,8)随机数
fmt.Printf("%d / ", r)
}
fmt.Println()
timens := int64(time.Now().Nanosecond())
rand.Seed(timens) //随机数种子
for i := 0; i < 10; i++ {
fmt.Printf("%2.2f / ", 100*rand.Float32())
}
}
你可以使用 rand.Seed(value) 函数来提供伪随机数的生成种子,一般情况下都会使用当前时间的纳秒级数字
3.3 字符类型
严格来说,字符类型并不是 Go 语言的一个类型,字符只是整数的特殊用例。byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题。例如:var ch byte = 'A'
;字符使用单引号括起来。
在 ASCII 码表中,'A' 的值是 65,而使用 16 进制表示则为 41,所以下面的写法是等效的: 1
2//`\x`是十六进制表示法
var ch byte = 65 或 var ch byte = '\x41'\
后面紧跟着长度为 3 的 八进制数,例如:\377。
不过 Go 同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。其实 rune 也是 Go 当中的一个类型,并且是 int32 的别名。
在书写 Unicode 字符时,需要在 16 进制数之前加上前缀