Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝
基础类型
整型
Go
语言同时提供了有符号和无符号类型的整数运算。这里有int8
、int16
、int32
和int64
四种截然不同大小的有符号整形数类型,分别对应8
、16
、32
、64bit
大小的有符号整形数,与此对应的是uint8
、uint16
、uint32
和uint64
四种无符号整形数类型Unicode
字符rune
类型是和int32
等价的类型,通常用于表示一个Unicode
码点。这两个名称可以互换使用。同样byte
也是uint8
类型的等价类型,byte
类型一般用于强调数值是一个原始的数据而不是一个小的整数
其中有符号整数采用2的补码形式表示,也就是最高
bit
位用作表示符号位,一个n-bit
的有符号数的值域是从-2^n-1到2^n-1-1。无符号整数的所有bit位都用于表示非负数,值域是0到2^n-1。例如,int8类型整数的值域是从-128到127,而uint8类型整数的值域是从0到255。无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等
尽管Go
语言提供了无符号数和运算,即使数值本身不可能出现负数我们还是倾向于使用有符号的**int**
类型,就像数组的长度那样,虽然使用uint
无符号类型似乎是一个更合理的选择。事实上,内置的len
函数返回一个有符号的int
,如果len(medals)
返回无符号数,那么i>=0
永远成立,死循环
类型不匹配
需要一个显式的转换将一个值从一种类型转化位另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型
上面的代码会报错,
invalid operation: apples + oranges (mismatched types int32 and int16)
最常见的修复方法是将它们都显式转型为一个常见类型
var compote = int(apples) + int(oranges)
对于每种类型T
,如果转换允许的话,类型转换操作T(x)
将x
转换为T
类型。许多整形数之间的相互转换并不会改变数值;它们只是告诉编译器如何解释这个值。但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度
位操作符
位操作运算符^
作为二元运算符时是按位异或(XOR
),当用作一元运算符时表示按位取反;也就是说,它返回一个每个bit
位都取反的数。位操作运算符&^
用于按位置零(ANDNOT
):表达式z=x&^y
结果z
的bit
位为0
,如果对应y
中bit
位为1
的话,否则对应的bit位等于x
相应的bit
位的值
算术上,一个
x<<n
左移运算等价于乘以2^n,一个x>>n
右移运算等价于除以2^n。 1111<<2
111100;1111>>2
0011
浮点数
Go
语言提供了两种精度的浮点数,float32
和float64
。它们的算术规范由IEEE754
浮点数国际标准定义
常量math.MaxFloat32
表示float32
能表示的最大数值,大约是3.4e38
;对应的math.MaxFloat64
常量大约是1.8e308
。它们分别能表示的最小值近似为1.4e-45和4.9e-324
两个浮点数之间不应该使用=或!=进行比较操作,高精度科学计算应该使用**math**
标准库
布尔型
一个布尔类型的值只有两种:true
和false
。if
和for
语句的条件部分都是布尔类型的值,并且==
和<
等比较操作也会产生布尔型的值。一元操作符!
对应逻辑非操作,因此!true
的值为false
布尔值可以和&&(AND)
和||(OR)
操作符结合,并且可能会有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不在被求值,因此下面的表达式总是安全的
s != "" && s[0] == 'x'
// s[0]操作如果应用于空字符串将会导致panic异常
因为&&
的优先级比||
高,下面形式的布尔表达式是不需要加小括弧的
布尔值并不会隐式转换为数字值**0**
或**1**
,反之亦然。必须使用一个显式的if语句辅助转换
func btoi(b bool) int {
if b {
return 1
}
return 0
}
字符串
一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括**byte**
值0
,但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8
编码的Unicode
码点(rune)
序列
内置的len函数可以返回一个字符串中的字节数目(不是**rune**
字符数目),索引操作s[i]
返回第i个字节的字节值,i
必须满足0≤i<len(s)
条件约束
字符
第i
个字节并不一定是字符串的第i
个字符,因为对于非ASCII
字符的UTF8
编码会要两个或多个字节。
字符串的工作方式:
- 子字符串操作
s[i:j]
基于原始的s
字符串的第i
个字节开始到第j
个字节(并不包含j
本身)生成一个新字符串。生成的新字符串将包含j-i
个字节 - 如果索引超出字符串范围或者j小于i的话将导致
panic
异常 - 不管
i
还是j
都可能被忽略,当它们被忽略时将采用0
作为开始位置,采用len(s)
作为结束的位置 +
操作符将两个字符串链接构造一个新字符串- 字符串可以用
==
和<
进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然
编码的顺序 字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。如将一个字符串追加到另一个字符串,这并不会导致原始的字符串值被改变,但是变量
s
将因为+=
语句持有一个新的字符串值,但t
依然是包含原先的字符串值s := "left foot" t := s s += ", right foot" fmt.Println(s) // "left foot, right foot" fmt.Println(t) // "left foot"
尝试修改字符串内部数据的操作也是被禁止的
不变性意味如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。同样,一个字符串s
和对应的子字符串切片s[7:]
的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存
unicode
Unicode
(http://unicode.org),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,**每个符号都分配一个唯一的**`**Unicode**`**码点**,`Unicode`码点对应`Go`语言中的`rune`整数类型
UTF-8
UTF8
是一个将Unicode
码点编码为字节序列的变长编码。(unicode
是字符集,UTF-8
是unicode
字符集的实现)UTF8
编码使用1
-4
个字节来表示每个Unicode
码点,ASCII
部分字符只使用1
个字节,常用字符部分使用2
或3
个字节表示。每个符号编码后第一个字节的高端bit
位用于表示总共有多少编码个字节。如果第一个字节的高端bit
为0
,则表示对应7bit
的ASCII
字符,ASCII
字符每个字符依然是一个字节,和传统的ASCII
编码兼容。如果第一个字节的高端bit
是110
,则说明需要2
个字节;后续的每个高端bit
都以10
开头
编码规则如下,
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
- 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
举例:
已知“严”的unicode
是4E25
(100111000100101
),根据上表,可以发现4E25
处在第三行的范围内(00000800-0000FFFF
),因此“严”的UTF-8
编码需要三个字节,即格式是“1110xxxx10xxxxxx10xxxxxx
”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x
,多出的位补0
。这样就得到了,“严”的UTF-8
编码是“111001001011100010100101
”,转换成十六进制就E4B8A5
。
优点:
UTF8
编码比较紧凑,完全兼容ASCII
码,并且可以自动同步:它可以通过向前回朔最多2
个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看- 没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索
Go
语言字符串面值中的Unicode
转义字符让我们可以通过Unicode
码点输入特殊的字符。有两种形式:\uhhhh
对应16bit
的码点值,\Uhhhhhhhh
对应32bit
的码点值,其中**h**
是一个十六进制数字,
package main
import "fmt"
func main() {
s := "\u4e16\u754c"
s2 := "\U00004e16\U0000754c"
fmt.Printf("%s\t%s\n",s, s2)
}
// out
// 世界 世界
例如,得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀
// 前缀测试
func HasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
// 后缀测试
func HasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
// 包含子串
func Contains(s, substr string) bool {
for i := 0; i < len(s); i++ {
if HasPrefix(s[i:], substr) {
return true
}
} return false
}
当包含中西混合字符的时候,例如
package main
import "fmt"
import "unicode/utf8"
func main() {
s := "abc你好"
fmt.Printf("%s\n",s)
fmt.Println(len(s))
fmt.Println(utf8.RuneCountInString(s))
}
// out :
// abc你好
// 9
// 5
为了处理这些真实的字符,我们需要一个UTF8解码器。unicode/utf8
包提供了该功能,我们可以这样使用,每一次调用DecodeRuneInString
函数都返回一个r
和长度,r
对应字符本身,长度对应r
采用UTF8
编码后的编码字节数目。长度可以用于更新第i
个字符在字符串中的字节索引位置
package main
import "fmt"
import "unicode/utf8"
func main() {
s := "abc你好"
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}
}
// 0 a
// 1 b
// 2 c
// 3 你
// 6 好
**Go**
语言的**range**
循环在处理字符串的时候,会自动隐式解码**UTF8**
字符串
package main
import "fmt"
func main() {
s := "abc你好"
for i, r := range s {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}
}
// 0 'a' 97
// 1 'b' 98
// 2 'c' 99
// 3 '你' 20320
// 6 '好' 22909
可以使用以下程序统计字符串长度,或者直接调用utf8.RuneCountInString()
package main
import "fmt"
func main() {
s := "abc你好"
n := 0
for _, _ = range s {
n++
}
fmt.Println(n)
}
// out: 6
rune
UTF8
字符串作为交换格式是非常方便的,但是在程序内部采用rune
序列可能更方便,因为rune
大小一致,支持数组索引和方便切割,如果是将一个[]rune
类型的Unicode
字符slice
或数组转为string
,则对它们进行UTF8
编码
package main
import "fmt"
func main() {
s := "abc你好"
r := []rune(s)
fmt.Printf("%x\t%x\t%x\n", r, r[0], r[3])
fmt.Println(string(r))
}
// out: [61 62 63 4f60 597d] 61 4f60
// abc你好
将一个整数转型为字符串意思是生成以只包含对应Unicode
码点字符的UTF8
字符串
如果对应码点的字符是无效的,则用’\uFFFD
无效字符作为替换
字符串和Byte
切片
标准库中有四个包对字符串处理尤为重要:bytes
、strings
、strconv
和unicode
包
strings
包提供了许多如字符串的查询、 替换、 比较、 截断、 拆分和合并等功能bytes
包也提供了很多类似功能(strings
)的函数, 但是针对和字符串有着相同结构的[]byte
类型。 因为字符串是只读的, 因此逐步构建字符串会导致很多分配和复制。 在这种情况下, 使用bytes.Buffer
类型将会更有效strconv
包提供了布尔型、 整型数、 浮点数和对应字符串的相互转换, 还提供了双引号转义相关的转换unicode
包提供了IsDigit
、IsLetter
、IsUpper
和IsLower
等类似功能, 它们用于给字符分类。 每个函数有一个单一的rune
类型的参数, 然后返回一个布尔值。 而像ToUpper
和ToLower
之类的转换函数将用于rune
字符的大小写转换。byte
一个字符串是包含的只读字节数组, 一旦创建, 是不可变的。 相比之下, 一个字节slice
的元素则可以自由地修改, 字符串和字节slice
也可以相互转换 ```go package main
import “fmt”
func main() { s := “AAATTT” b := []byte(s) b[1] = 66 // 修改元素 s2 := string(b) fmt.Println(s) fmt.Println(b) fmt.Println(s2) } // out // AAATTT // [65 66 65 84 84 84] // ABATTT
一个`[]byte(s)`转换是分配了一个**新的字节数组**用于保存字符串数据的拷贝,** 然后引用这个底层的字节数组**。 编译器的优化可以避免在一些场景下分配和复制字符串数据, **但总的来说需要确保在变量**`**b**`**被修改的情况下, 原始的s字符串也不会改变**。 将一个字节`slice`转到字符串的`string(b)`操作则是**构造一个字符串拷贝**, 以确保`s2`字符串是**只读**的<br />`bytes`包和`strings`同时提供了许多实用函数。 下面是`strings`包中的六个函数
```go
package main
import (
"fmt"
"strings"
)
func main() {
s1 := "AAATTT"
s2 := "ASSSSA"
s3 := []string{s1, s2}
fmt.Println(strings.Contains(s1, "AAA")) // func Contains(s, substr string) bool
fmt.Println(strings.Count(s2, "A")) // func Count(s, sep string) int
fmt.Println(strings.Fields(s2)) // func Fields(s string) []string, 转换为字符串数组
fmt.Println(strings.HasPrefix(s2, "SSS")) // func HasPrefix(s, prefix string) bool, 相当于python的str.startswith()
fmt.Println(strings.Index(s2, "SSS")) // func Index(s, sep string) int, 相当于python的str.find()
fmt.Println(strings.Join(s3, "--+--")) // func Join(a []string, sep string) string
}
// out
// true
// 2
// [ASSSSA]
// false
// -1
// AAATTT--+--ASSSSA
同样,byte
包也有这几种函数,用法一致,只是string
的输入变为[]byte
字符串和数字的转换
由strconv
包提供这类转换功能
将一个整数转为字符串, 一种方法是用**fmt.Sprintf**
返回一个格式化的字符串; 另一个方法是用strconv.Itoa(“整数到ASCII”)
:
package main
import (
"fmt"
"strconv"
)
func main() {
s1 := 65
s2 := 66
y := fmt.Sprintf("%d", s1)
fmt.Println(y)
fmt.Println(strconv.Itoa(s2) + y)
fmt.Println(s1 + s2)
}
// out
// 65
// 6665
// 131
常量
常量表达式的值在编译期计算, 而不是在运行期。 每种常量的潜在类型都是基础类型:boolean
、 string
或数字
一个常量的声明语句定义了常量的名字, 和变量的声明语法类似, 常量的值不可修改, 这样可以防止在运行期被意外或恶意的修改。常量间的所有算术运算、 逻辑运算和比较运算的结果也是常量, 对常量的类型转换操作或以下函数调用都是返回常量结果: len
、 cap
、 real
、 imag
、 complex
和unsafe.Sizeof
常量的声明
const pi = 3.14159
// 多个
const (
pi = 3.14159
e = 2.71828
)
常量可以是构成类型的一部分, 例如用于指定数组类型的长度
一个常量的声明也可以包含一个类型和一个值, 但是如果没有显式指明类型, 那么将从右边的表达式推断类型time.Duration
是一个命名类型, 底层类型是int64
,time.Minute
是对应类型的常量。 下面声明的两个常量都是time.Duration
类型, 可以通过%T参数打印类型信息
如果是批量声明的常量, 除了第一个外其它的常量右边的初始化表达式都可以省略, 如果省略初始化表达式则表示使用前面常量的初始化表达式写法, 对应的常量类型也一样的
const (
a = 1
b // b = 1
c = 2
d // d = 2
)
iota 常量生成器
常量声明可以使用iota
常量生成器初始化, 它用于生成一组以相似规则初始化的常量, 但是不用每行都写一遍初始化表达式。 在一个const
声明语句中, 在第一个声明的常量所在的行,**iota**
将会被置为**0**
, 然后在每一个有常量声明的行+1
type Weekday int
const (
Sunday Weekday = iota // Sunday = 0
Monday // Monday = 1
Tuesday // Tuesday = 2
Wednesday // Wednesday = 3
Thursday
Friday
Saturday
)
生成KB
MB``GB
…的一系列常量
const (
_ = 1 << (10 * iota) // 1 (10 * 0)
KiB // 1024 --> 10000000000 (10 * 1)
MiB // 1024 * 1024 --> 10000000000000000000 (10 * 2)
GiB
TiB
PiB
Eib
)
无类型常量
Go
语言的常量有个不同寻常之处。 虽然一个常量可以有任意有一个确定的基础类型, 例如int
或float64
, 或者是类似time.Duration
这样命名的基础类型, 但是许多常量并没有一个明确的基础类型。 编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算; 你可以认为至少有256bit
的运算精度。 这里有六种未明确类型的常量类型
- 无类型的布尔型
- 无类型的整数
- 无类型的字符
- 无类型的浮点数
- 无类型的复数
- 无类型的字符串
例如上面的例子,ZiB
和YiB
的值已经超出任何Go语言中整数类型能表达的范围, 但是它们依然是合法的常量, 而且可以像下面常量表达式依然有效math.Pi
无类型的浮点数常量, 可以直接用于任意需要浮点数或复数的地方,如果math.Pi
被确定为特定类型, 比如float64
, 那么结果精度可能会不一样, 同时对于需要float32
或complex128
类型值的地方则会强制需要一个明确的类型转换
只有常量可以是无类型的。 当一个无类型的常量被赋值给一个变量的时候,或表达式中含有明确类型的值, 无类型的常量将会被隐式转换为对应的类型, 如果转换合法的话无论是隐式或显式转换, 将一种类型转换为另一种类型都要求目标可以表示原始值。 对于浮点数和复数, 可能会有舍入处理,对于一个没有显式类型的变量声明语法( 包括短变量声明语法) , 无类型的常量会被隐式转为默认的变量类型