数据类型
数字类型
类型 | 描述 |
---|---|
int8/int16/int32/int64 | 有符号整数 |
uint8/uint16/uint32/uint64 | 无符号整数 |
float32/float64 | IEEE-754 32/64位浮点型数; 与Java的对应:float32 => float;float64 => double 自动类型推导为 float64 开发中建议尽量使用 float64,因为 math 包下面的计算都是用此类型 |
byte | 类似uint8 |
rune | 类似 int32 |
uint | 32或64位 |
int | 与 uint 一样大小 |
unitptr | 无符号整型,用于存放一个指针 |
八进制:增加前缀 0
来表示八进制数(如:077)
十六进制:增加前缀 0x
来表示十六进制数(如:0xFF)
10
的幂运算:使用 E
来表示10
的连乘(如:1E3 = 1*10^3 = 1000
)
永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等
(转化为二进制时会损失精度)
// 浮点数比较方案
func CompareFloat() {
var floatValue1 float32
floatValue1 = 10
floatValue2 := 10.0
p := 0.00001
// 使用 math 函数代替 == 比较
if math.Dim(float64(floatValue1), floatValue2) < p {
fmt.Println("floatValue1 和 floatValue2 相等")
}
}
复数类型
类型 | 描述 |
---|---|
complex64/complex128 | 32/64位实部和虚部 |
与复数相对,我们可以把整型和浮点型这种日常比较常见的数字称为实数
复数是实数的延伸。通过两个实数(计算机中使用浮点数表示)构成,实部(real)和虚部(imag)
// a b 均为实数,i 称为虚数单位
// a = 0,z 为普通实数
// a ≠ 0,z 为纯虚数
z = a + bi
z := complex(a, b)
// 获取实部
a := real(z)
// 获取虚部
b := imag(z)
推荐使用 complex128
作为计算类型,因为相关函数大都使用这个类型的参数
字符串
标准库API:strings package - strings - pkg.go.dev
默认通过UTF-8
编码的字符序列,当字符为ASCII
码时则占用1
个字节,其它字符根据需要占用2-4
个字节
(可以包含非ANSI
字符,如:「Hello, 学院君」)
Go 语言标准库并没有内置的编码转换支持
是一种不可变值类型
不支持单引号
对特定字符进行转义,可以通过 \
实现
常见需要转义字符:
\n
:换行符\r
:回车符\t
:tab 键\u
或 \U :Unicode 字符\\
:反斜杠自身
可以使用`
构建多行字符串(+
也可以实现)
func TestString() {
results := `
first line
second line
third line`
fmt.Println("results:", results)
}
切片:
// 是一个左闭右开的区间
func strSplice() {
str := "hello, world"
str1 := str[:5] // 获取索引5(不含)之前的子串
str2 := str[7:] // 获取索引7(含)之后的子串
str3 := str[0:5] // 获取从索引0(含)到索引5(不含)之间的子串
}
遍历方式:
- Unicode字符遍历:[for 循环](#for 循环)
- 字节数组遍历
// 两种方式中英文字符串遍历结果不同
// 字节数组遍历
for i := 0; i < len(str); i++ {
// 依据下标取字符串中的字符,值类型为 byte
fmt.Println("index:", i, ",value:", str[i])
}
// Unicode 字符遍历
str := "Hello, 世界"
for i, ch := range str {
// ch 的类型为 rune
fmt.Println(i, ch)
}
底层字符类型(对字符串中的单个字符进行了单独的类型支持):
byte
,代表UTF-8
编码中单个字节的值
(uint8
类型的别名,两者是等价的,因为正好占据 1 个字节的内存空间)rune
,代表单个 Unicode 字符
(int32
类型的别名,正好占据 4 个字节的内存空间。rune
操作可查阅 Go 标准库 unicode )
将Unicode
字符编码转化为对应的字符,可以使用 string
函数进行转化
数组
数组是值类型
一维
// 声明(支持语法糖省略长度声明,编译期自动计算长度)
// 声明时数组的长度为一个常量或一个常量表达式(编译期即可计算结果的表达式)
var variable_name [capacity]data_type{element_values}
var variable_name [...]data_type{element_values}
var variable_name [capacity]data_type{index:value,index:value} //设置指定下标的值
// 初始化
// SIZE可以不写(括号必需保留),会自动根据值的个数进行推到并设置
var variable_name = [SIZE]variable_type{val_1,val_2,...,val_SIZE}
var variable_name = []variable_type{val_1,val_2,...,val_SIZE}
// 访问略
// 示例
var balance [10] int
balance := []int{1, 2, 5, 7, 8, 9, 3}
// 长度不满零值填充
balance := [10]int{1, 2, 5, 7, 8, 9, 3}
// 指定下标值,其余零值填充
balance := [10]int{1:2, 5:7}
多维
// 声明
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
// 示例(二维数组)
var a1 [3][4] int
var a2 = [3][4]int{
{0, 1, 2, 3}, /* 第一行索引为 0 */
{4, 5, 6, 7}, /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}
遍历参考[for 循环](#for 循环)
不指定长度的定义或引用,其实是切片(Slice)
作为形参
作为形参时,参数数组的长度必需与传入的数组一致
func method(arr [SIZE]type) [return_types] {}
// 没有长度的是切片
func method(arr []type) [return_types] {}
切片(Slice)
切片与数组的区别:
- 切片的类型字面量中只有元素的类型,没有长度
- 切片的长度可以随着元素数量的增长而增长(但不会随着元素数量的减少而减少)
- 切片是对数组的抽象
- 切片长度不固定,支持追加元素,支持动态扩容(数组长度不可变,切片就是为了解决这个问题)
- 切片未初始化之前会填充元素类型对应的零值,此时:
len=cap
- 遍历参考[for 循环](#for 循环)
- 删除元素:通过切片的切片实现伪删除;
操作图示:Go Slice Tricks Cheat Sheet (ueokande.github.io) - 数据共享:基于切片创建切片时(两者数组指针都指向同一个数组),修改任意一个切片都会影响到另一个切片(解决:使用
append
方法*append
方法会重新分配新的内存*) 切片是引用类型
// 声明
// 1、常规声明(相比数组不定义长度)
var identifier []type = []data_type{v1, v2, v3, ... , vN}
// 2、使用 make() 声明
var identifier []type = make([]type, len)
identifier := make([]type, len [,cap])
// 3、基于数组|切片(通过数组|切片截取产生)
identifier := predefined_array|predefined_slice[startInclude:endExclude]
identifier := predefined_array|predefined_slice[startInclude:]
identifier := predefined_array|predefined_slice[:endExclude]
identifier := predefined_array|predefined_slice[:]
// 元素追加
old_slice = append(numbers, 1,2,3,4)
// 追加另一个切片
old_slice = append(numbers, other_slice...)
// 拷贝(以较小的长度为准;都是复制前N个元素)
copy(numbers1,numbers)
// 元素删除
slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 使用append创建新的slice会分配新的内存空间,可以避免数据共享问题
slice4 := append(slice3[:0], slice3[3:]...) // append实现,删除前三个元素
slice5 := append(slice3[:1], slice3[4:]...) // append实现,删除中间的三个元素
slice6 := append(slice3[:0], slice3[:7]...) // append实现,删除最后三个元素
slice7 := slice3[:copy(slice3, slice3[3:])] // copy实现,删除开头前三个元素
事实上,使用直接创建的方式来创建切片时,Go 底层还是会有一个匿名数组被创建出来,然后调用基于数组创建切片的方式返回切片,只是上层不需要关心这个匿名数组的操作而已。
所以,最终切片都是基于数组创建的,切片可以看做是操作数组的指针
指针&长度&容量解释:
指针:指向数组起始下标
长度:对应切片中元素的个数
容量:切片起始位置到底层数组结尾的位置(可分配的存储空间)
一个切片的容量初始值根据创建方式的不同而不同:
- 对于基于数组和切片创建的切片,默认容量是从切片起始索引到对应底层数组的结尾索引
- 对于通过内置
make
函数创建的切片,在没有指定容量参数的情况下,默认容量和切片长度一致
字典(Map)
- 使用
hash
实现,是一种无序的键值对集合 - 如果仅仅是声明,此时
map = nil
,不能赋值;必需初始化后才能进行赋值
【初始化方式:make()
或直接初始化】 - 声明
map
的key
类型时,要求数据类型必须是支持通过==
或!=
进行判等操作的类型;为了提高性能,类型长度越短越好(通常设置为整型或长度较短的字符串)
【底层使用哈希表实现;出现哈希冲突时,使用原始键判等】
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable = make(map[key_data_type]value_data_type)
// 判断key是否存在于map中
// 如果exists=true,则value为对应的值
value, exists := map_variable[key]
// 删除元素
delete(map_variable, key)
// 示例
// 分别定义(或者 := 定义并初始化)
var testMap = map[string]int
// 此时 testMap == nil,不能添加键值对
// testMap["three"] = 3 // panic: assignment to entry in nil map
// 1、直接初始化
testMap = map[string]int{
"one": 1,
"two": 2,
}
// 2、使用 make() 初始化
testMap = make(map[string]int)
testMap["one"] = 1
testMap["two"] = 2
指针与unsafe.Pointer
变量本质是对一块内存空间的命名,可以通过引用变量名来使用这块内存空间存储的值;指针则用来指向这些变量值所在的内存地址的值
- 指针变量通常缩写为
ptr
- 赋值必需是对应变量的内存地址,即
ptr = &var_name
- 在指针类型前面加上
*
号(val = *ptr
,间接引用符)来获取指针所指向的内容 - 格式化输出时,可以通过
%p
来标识指针类型
- 为开发者提供操作变量对应内存数据结构的能力
- 提高程序的性能
指针指向的内存地址的大小是固定的,32位机器上占 4 个字节,64位机器上占 8 个字节
(与指向内存地址存储的值类型无关)
// 声明
var var_name *var_type
var var_name new(var_type)
// 示例
a := 100
var ptr *int // 声明为指针类型 ==> 本身是一个内存地址值,需要通过内存地址进行赋值
ptr = &a // 初始化指针类型值为变量a的内存地址(& 可以获取变量所在的内存地址)
fmt.Println(ptr) // 内存地址:0xc0000aa058
fmt.Println(*ptr) // 该地址内存储的值:100
var ip *int /* 指向整型 */
var fp *float32 /* 指向浮点型 */
空指针
- 当一个指针被定义后没有分配到任何变量时,它的值为
nil
,即空指针。 - 在概念上
nil
和其它语言的null、None、nil、NULL一样,都指代零值或空值。
unsafe.Pointer
unsafe.Pointer
是一个万能指针,可在任何指针类型之间做转化,绕过了Go的安全机制,
是一个不安全的操作uintptr
是 Go 内置的可用于存储指针的整型,而整型是可以进行数学运算的!因此,将unsafe.Pointer
转化为uintptr
类型后,就可以让本不具备运算能力的指针具备了指针运算能力
是特别定义的一种指针类型,可以包含任意类型变量的地址
官方说明:
- 任何类型的指针都可以被转化为
unsafe.Pointer
; unsafe.Pointer
可以被转化为任何类型的指针;uintptr
可以被转化为unsafe.Pointer
;unsafe.Pointer
可以被转化为uintptr
。
// 规则 1 2
i := 10
var p *int = &i
// int 类型指针先转换为 unsafe.Pointer,再转换为 *float32
var fp *float32 = (*float32)(unsafe.Pointer(p))
*fp = *fp * 10
fmt.Println(i) // i=100
// 规则 3 4
arr := [3]int{1, 2, 3}
ap := &arr
// unsafe.Sizeof 获取数组元素偏移量
// 获取到arr的指针,通过unsafe.Pointer转化为uintptr类型,再加上数组元素偏移量,
// 即得到该数组第二个元素的内存地址,后通过unsafe.Pointer将其转化为int类型指针赋值给sp指针,并进行修改
sp := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(ap)) + unsafe.Sizeof(arr[0])))
*sp += 3
fmt.Println(arr) // arr=[1 5 3]
指针数组
var ptr [SIZE]*type
// 示例:整数型指针数组
var ptr [5]*int
指向指针的指针
一个指针变量存放的是另一个指针变量的地址
var ptr **type
// 示例
var ptr **int
访问指向指针的指针变量值需要使用两个*
号
func method() {
a := 3000
var ptr *int
var pptr **int
/* 指针 ptr 地址 */
ptr = &a
/* 指向指针 ptr 地址 */
pptr = &ptr
/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a)
fmt.Printf("指针变量 *ptr = %d\n", *ptr)
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
指针作为形参
func method(x, y *var_type) {
// fixme 内部使用都需要满足 *x *y 格式(都是通过内存地址操作对应的值)
}
流程控制
条件语句
// if
if condition {
// do something
}
// if...else...
if condition {
// do something
} else {
// do something
}
// if...else if...else...
if condition1 {
// do something
} else if condition2 {
// do something else
} else {
// catch-all or default
}
- 条件语句不需要使用圆括号将条件包含起来
()
; - 无论语句体内有几条语句,花括号
{}
都是必须存在的; - 左花括号
{
必须与if
或者else
处于同一行; - 在
if
之后,条件语句之前,可以添加变量初始化语句,使用;
间隔,
比如:if score := 100; score > 90 { work_code }
分支语句
Switch
-
不需要用
break
来明确退出一个case
只有在case
中明确添加fallthrough
关键字,才会继续执行紧跟的下一个case
-
单个
case
中,可以出现多个结果选项(通过逗号,
分隔) -
所有
case
候选值必需同switch
变量(表达式)相同类型(否则编译错误) -
有两种写法
//变量 var_name 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值 // 1、精确匹配 switch var_name { case val1: // 匹配项中不需要添加 break(在下一个case出现之前当前case自动结束) ... case val2, val3: // 合并分支;case 中可以存在多个 value ... case val4: // 如果一个case中没有业务逻辑,Go认为这是一个空语句,会直接退出(也不会执行default) // 如果希望当前case执行完成后继续执行下一个case,声明一个 fallthrough 即可 case val5: // 业务代码 ... default: ... } // 2、条件匹配(不设定switch之后的表达式) var_name := some value switch { case condition(A): // 业务代码 case condition(B): // 业务代码 default: // 默认处理 }
Type Switch
(fixme)
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型
switch x.(type){
case type:
statement(s)
case type:
statement(s)
/* 你可以定义任意个数的case */
default: /* 可选 */
statement(s)
}
实例:
func method() {
// 定义接口
var x interface{}
switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T", i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
// case 项也可以是 func
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
// 可以测试多个可能符合条件的值
case bool, string:
fmt.Printf("x 是 bool 或 string 型")
default:
fmt.Printf("未知型")
}
}
Select
(fixme)
- 与操作系统中的
select
比较相似- 与
switch
有相似的控制结构,但这些case
中的表达式必须都是Channel
的收发操作
- 是一个控制结构;
- select 随机执行一个可运行的 case;
- 如果没有 case 可运行,select 将阻塞,直到有 case 可运行;
- 默认的 default (如果存在)必需可以正常执行
select {
case communication clause :
statement(s)
case communication clause :
statement(s)
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s)
}
- 每个 case 都必须是一个
Channel
- 所有
Channel
表达式都会被求值 - 所有被发送的表达式都会被求值
- 如果任意某个
Channel
可以执行,它就执行;其他被忽略 - 如果有多个 case 可以运行,select 会随机公平的选出一个执行;其他被忽略
- 如果没有 case 可以运行:
- 存在 default ,执行 default
- 没有 default,阻塞直到某个
Channel
可以进行(Go 不会对 channel 或值进行求值)
select的知识点小结如下:
- select 语句只能用于信道的读写操作
- select 中的 case 条件(非阻塞)是并发执行的,select 会选择先操作成功的那个 case 条件去执行,如果多个同时返回,则随机选择一个执行,此时将无法保证执行顺序。对于阻塞的 case 语句会直到其中有信道可以操作,如果有多个信道可操作,会随机选择其中一个 case 执行
- 对于 case 条件语句中,如果存在信道值为 nil 的读写操作,则该分支将被忽略,可以理解为从 select 语句中删除了这个 case 语句
- 如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的 case ,则执行这个超时 case 。如果此段时间内出现了可操作的 case ,则直接执行这个 case 。一般用超时语句代替 default 语句
- 对于空的 select{} ,会引起死锁
- 对于 for 中的 select{} ,也有可能会引起 cpu 占用过高的问题
循环语句
- 不支持
whie
和do-while
结构的循环语句 - 可以通过
for-range
结构对可迭代集合进行遍历 - 支持
continue
和break
来控制循环 - 支持高级的
break
停止指定循环:break label
(label
是自定义的循环名称,同Java
)
循环类型
for 循环
// 三种形式,只有一种使用分号
// 1、与 C 的 for 同(同 Java ,没有括号)
for init; condition; post { }
for i := start; i < end; i++ { }
// 2、与 C 的 while 同
for condition { }
// 3、与 C 的 for(;;) 同
for { }
// 使用 break 结束循环
【for-range
】for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环
for key, value := range oldMap {
newMap[key] = value
}
示例:
func method() {
numbers := [6]int{1, 2, 3, 5}
for i, x := range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i, x)
}
}
嵌套循环
// 同 Java ,没有括号
for [condition | ( init; condition; increment ) | Range]
{
for [condition | ( init; condition; increment ) | Range]
{
statement(s)
}
statement(s)
}
跳转语句
break & contine
- 通过
break
语句跳出循环,通过continue
语句进入下一个循环 - 高级的
break
停止指定循环:break label
(label
是自定义的循环名称,同Java
)
goto
(不建议使用)
可以无条件的转移到过程中指定的行
通常与条件语句配合使用,实现条件转移,构成循环、跳出循环的等功能
一般不建议使用goto
,以免造成程序流程混乱,使程序难以理解或调试困难
参考资料: