基础
关键字
关键字有25个。关键字不能用于自定义名字,只能在特定语法结构中使用。
|
|
预定义名字
大约有30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。
|
|
其中:
- byte 是 uint8 类型的别名,存储 raw data
- rune 是 int32 类型的别名,存储一个 Unicode code point 字符
这些内部预先定义的名字并不是关键字,你可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。
命名规则
Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。
如果一个名字是在函数内部定义,那么它的就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问。
命名的建议:
- 推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。
- 包本身的名字一般总是用小写字母。
- 名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样。
- 如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。
声明和变量
- var 用于声明变量
- const 用于声明常量
- type 用于声明一个新的类型
- func 用于声明一个函数
|
|
- var 形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
- 简短模式有以下限制:
- 定义变量,同时显式初始化。
- 不能提供数据类型。
- 只能用在函数内部。
- 由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。
- 在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错。
- 当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil 等。所有的内存在 Go 中都是经过初始化的。
匿名变量
匿名变量的特点是一个下画线“”,“”本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。
|
|
变量的生命周期
- 对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。
- 局部变量的声明周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。
- 函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
常量
常量表达式的值在编译期计算,而不是在运行期。
iota 常量生成器
常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。
|
|
new 和 make
new 和 make 是两个内置函数,主要用来创建并分配类型的内存。new 只分配内存,而 make 只能用于 slice、map 和 channel 的初始化。
- new 函数只接受一个参数,这个参数是一个类型,并且返回一个指向该类型内存地址的指针。同时 new 函数会把分配的内存置为零,也就是类型的零值。
|
|
- make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及 slice 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
运算符
运算符 | 描述 | ||
---|---|---|---|
+ |
加 | ||
- |
减 | ||
* |
乘 | ||
/ |
除 | ||
% |
取余 | ||
& |
按位与 | ||
` | ` | 按位或 | |
^ |
按位异或 | ||
&^ |
按位清除(AND NOT)&^ 即 AND NOT(x, y) = AND(x, NOT(Y)) |
||
<< |
左移 | ||
>> |
右移 | ||
== |
相等 | ||
!= |
不等 | ||
< |
小于 | ||
<= |
小于等于 | ||
> |
大于 | ||
>= |
大于等于 | ||
&& |
逻辑与 | ||
` | ` | 逻辑或 | |
! |
取反 | ||
& |
寻址(生成指针) | ||
* |
获取指针指向的数据 | ||
<- |
向 channel 中发送 / 接收数据 |
包( package )
- main package 才是可执行文件
- package 名字与 import 路径的最后一个单词一致(如导入 math/rand 则 package 叫 rand)
- 写开头的标识符(变量名、函数名…),对其他 package 是可访问的
- 小写开头的标识符,对其他 package 是不可见的
包的初始化
- 包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化。
- 如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。
- 每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。
- 初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。
init初始化函数
对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数。
|
|
数据类型
数据类型分为:基础类型、复合类型、引用类型和接口类型。
- 基础类型,包括:数字、字符串和布尔型等
- 复合数据类型:数组和结构体等
- 引用类型包括:指针、切片、字典、函数、通道等
基础类型
- uint 32 位或 64 位
- uint8 无符号 8 位整型 (0 到 255)
- uint16 无符号 16 位整型 (0 到 65535)
- uint32 无符号 32 位整型 (0 到 4294967295)
- uint64 无符号 64 位整型 (0 到 18446744073709551615)
- int 32 位或 64 位
- int8 有符号 8 位整型 (-128 到 127)
- int16 有符号 16 位整型 (-32768 到 32767)
- int32 有符号 32 位整型 (-2147483648 到 2147483647)
- int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)
- byte uint8 的别名(type byte = uint8)
- rune int32 的别名(type rune = int32),表示一个 unicode 码
- uintptr “无符号整型,用于存放一个指针是一种无符号的整数类型,没有指定具体的 bit 大小但是足以容纳指针。uintptr 类型只有在底层编程是才需要,特别是Go 语言和 C 语言函数库或操作系统接口相交互的地方。”
- float32 IEEE-754 32 位浮点型数
- float64 IEEE-754 64 位浮点型数
- complex64 32 位实数和虚数
- complex128 64 位实数和虚数
- string 字符串,默认值是空字符串,而非 NULL
复合数据类型
复合数据类型类型包括数组、结构体、切片、字典等。
struct
Go 语言中没有 class 类的概念,取而代之的是 struct,struct 的方法对应到类的成员函数。
结构类型可以用来描述一组数据值,这组值的本质既可以是原始的,也可以是非原始的。
|
|
匿名结构体
使用 map[string]interface{}
开销更小且更为安全。
|
|
array
|
|
list
在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。
初始化:
- 变量名 := list.New()
- var 变量名 list.List
|
|
slice
切片是有三个字段的数据结构,指向底层数组的指针,切片访问元素的个数(长度),切片允许增长到的元素的个数(容量),这种数据结构便于使用和管理数据集。切片围绕动态数组的概念构建的,可以按需自动增长和缩小。切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法,切片的底层内存是在连续块中分配的。
语法:
slice [开始位置 : 结束位置]
- slice:表示目标切片对象;
- 开始位置:对应目标切片对象的索引;
- 结束位置:对应目标切片的结束索引。
从数组或切片生成新的切片拥有如下特性:
- 取出的元素数量为:结束位置 - 开始位置;
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
- 当缺省开始位置时,表示从连续区域开头到结束位置;
- 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
- 两者同时缺省时,与切片本身等效;
- 两者同时为 0 时,等效于空切片,一般用于切片复位。
根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。
|
|
map
map (映射)是一种数据结构,用于存储一系列无序的键值对。
|
|
Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。1.9 版本中提供了一种效率较高的并发安全的 sync.Map。
指针
一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。
当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。指针变量通常缩写为 ptr。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用在变量名前面添加 &操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:
ptr := &v // v 的类型为 T
==其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。==
变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。
从指针获取指针指向的值: 当使用&
操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*
操作符,也就是指针取值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。
*操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。
new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。
通道
如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。
通道的声明如下:
|
|
如: ch := make(chan int) // ch has type 'chan int'
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。
channel的发送和接受操作:
- 一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine
- 发送和接收两个操作都使用
<-
运算符 - 在发送语句中,<-运算符分割channel和要发送的值
- 在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的
示例如下:
|
|
不带缓存的channel
- 一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作。
- 如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
- 无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在唤醒发送者goroutine之前。
串联的Channels(Pipeline)
Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。
示例:
|
|
单方向的Channel
Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。箭头<-
和关键字chan的相对位置表明了channel的方向。
示例:
|
|
带缓存的Channels
带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。
向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。
数据类型转换
由于Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明:
|
|
只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)。
函数
函数的基本语法为:
|
|
基本函数
|
|
匿名函数(函数作为值和回调使用)
匿名函数语法为:
|
|
一些示例:
|
|
闭包
函数 + 引用环境 = 闭包
|
|
可变参数函数
|
|
range 函数
|
|
流程控制
if…else…
|
|
switch…case…
case 语句自带 break,想执行所有 case 需要手动 fallthrough
|
|
for
Go 语言中循环结构只有 for,没有 do、while、foreach 等
|
|
错误处理
处理运行时错误
Go 中没有异常处理机制,函数在调用时在有可能会产生错误,可返回一个 Error
类型的值,Error
接口:
|
|
自定义一个错误:
|
|
一个可能产生错误的函数:
|
|
defer(延迟错误处理)
Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
关键字 defer 的用法类似于面向对象编程语言 Java 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。因为 defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。
宕机(panic)与宕机恢复(recover)
Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于 panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用Go语言提供的错误机制,而不是 panic。
手动触发宕机: panic("crash")
Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。
panic 和 recover 的关系
panic 和 recover 的组合有如下特性:
- 有 panic 没 recover,程序宕机。
- 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。
提示
虽然 panic/recover 能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。
在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃。
如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。
OOP
对象
见 struct
继承
Go语言中的继承是通过内嵌或组合来实现的
- 1) 内嵌的结构体可以直接访问其成员变量
- 2) 内嵌结构体的字段名是它的类型名
|
|
构造方法
Go语言的类型或结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数。
|
|
方法
Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。
接收器语法格式:
|
|
对各部分的说明:
- 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。
- 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:格式与函数定义一致。
接收器根据接收器的类型可以分为指针接收器、非指针接收器,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。
指针接收器
指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。
由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。
|
|
非指针接收器
当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效。
|
|
指针和非指针接收器的使用
在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。
不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉C或者C艹的人这里应该很快能明白。
接口
接口是用来定义行为的类型,这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。Go 接口实现机制很简洁,只要目标类型方法包含接口声明的全部方法,就被视为实现了该接口,无需做显示声明。当然,目标类可以实现多个接口。如果接口没有任何方法声明,那么就是一个空接口(interface{}),它的用途类似 Object,可被赋值为任何类型的对象。
|
|
类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。
value, ok := x.(T)
其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。
该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:
- 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
- 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
- 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
并发
goroutine
goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。
Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。
使用普通函数创建 goroutine:
Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。语法:go 函数名( 参数列表 )
。使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略。如果需要在 goroutine 中返回数据,请使用后面介绍的通道(channel)特性,通过通道把数据从 goroutine 中作为返回值传出。
也可以使用匿名函数创建:
|
|
示例:
|
|
反射
Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。
反射是由 reflect 包提供的。 它定义了两个重要的类型, Type 和 Value。
- 一个 Type 表示一个Go类型。函数 reflect.TypeOf 接受任意的 interface{} 类型, 并以reflect.Type形式返回其动态类型
- 一个 reflect.Value 可以装载任意类型的值。函数 reflect.ValueOf 接受任意的 interface{} 类型, 并返回一个装载着其动态值的 reflect.Value。