四种基本类型:基础类型(basci type)、聚合类型(aggregate type)、引用类型(referemce type)、接口类型(interface type)。
基础类型:number、string、boolean。 聚合类型:arrary、struct。 引用类型:pointer、slice、map、function、channel。 接口类型:等待第七章
3.1 整数
有符号 | int | int8 | int16 | int32 (rune) | int64 | \ |
---|---|---|---|---|---|---|
无符号 | uint | uint8 (byte) | uint16 | uint32 | uint64 | unitptr |
后四对都指定了bit占用;int与uint在平台上的大小与原生的有符号整数/无符号整数相同,或等于该平台上的运算效率最高的值。
// Windows 11 23H2 x86-64
package main
import "unsafe"
func main() {
var a int = 1
println(unsafe.Sizeof(a)) // 8
var b int32 = 1
println(unsafe.Sizeof(b)) // 4
var c uint = 1
println(unsafe.Sizeof©) // 8
var d uint32 = 1
println(unsafe.Sizeof(d)) // 4
}
rune是int32的可替换使用同义词;rune常常用于指明值是以一个Unicode码点。 byte是uint8的可替换使用同义词;强调是原始数据而非量值。 uintptr大小不明确但可以存下整个指针。适合底层编程。 Unicode:32/8=4B,UCS-4或UTF-32标准。UCS-2标准是2B
表中的每一项都是单独一种类型,尽管int、uint、(以及uintptr)在某些平台下与int32/64等等是等长的,但是它们是不同类型,必须显示转换。
二元运算符:
- *、/、%、<<、>>、&、&^(位清空 AND NOT)
- +、-、|、^
- ==、!=、<、<=、>、>=
- &&
- ||
+-*/用于整、浮点、复数。%仅整数,结果与被除数一致。/的操作数如果都是整型则结果也是,否则不是。 ^作为二元运算符是亦或(XOR);作为一元运算符表示按位取反。 &^:z=x&^y -->> z=x&(^y) 在关系上是这样的。
位集(bitset):是一组布尔值,但是每个布尔值仅占用1位空间。
设有一个8位的位集var bset int8
,从高到低编号为76543210,如果集合表示为{1,5}则有: bset = 1<<1 | 1<<5
,这时底层表示为00100010
,代表1、5号为ture。 为什么呢?因为如果bset=1
就是赋值底层为00000001
,bset=1<<1
是左移一位也就是00000010
; 而或‘|’就是“链接”00000010
和00100000
。 在此之后bset
就成了位集(集合),可以进行逻辑运算(只是要按位运算)。
一般而言,无符号数只用于位运算符和特定算术运算符,如:实现bitset、解析俄日禁止二进制格式文件、散列或加密。而极少表示非负值。 例如:var i uint = 0
如果这是循环的index就有可能出现i-1
的情况,而现在的i-1
变为最大值(2^n-1)-1
,就会导致判断错误。 其他语言也这样(C++):
#include<iostream>
int main(){
unsigned int a=0;
std::cout<<a-1; // 4294967295
}
package main
func main() {
var a uint = 0
println(a - 1) // 18446744073709551615
}
# 这里缺少关于Printf()的谓词 #
3.2 浮点数
float32和float64,遵守IEEE 754标准。 Math包给出了浮点极限值:math.Maxfloat32
调用。 十进制下float32和64的有效位大约是6位和15位。 关于Printf的谓词%g
可以保证浮点的精度输出。同样保证精度的还有%e
(有指数)、%f
(无指数),常用于数据表。
IEEE 754也定义了特殊值±∞和NaN分别表示超出最大许可值的数以及除0的商和数学上无意义的运算(0/0或Sqrt(-1))。也可以通过api调用。 值得一提的是:在数字运算中我们通常倾向将NaN当作信号值(sentinel value)而非数字。可以通过math.IsNaN()判断;同时直接判断是否为NaN可能存在错误,因为与NaN比较总不成立。(除了!=,因为它总与==相反)。
nan = math.NaN()
fmt.Printl(nan == nan, nan < nan, nan > nan) // false, fales, fales
一个函数的返回值是浮点且有可能出错则最好单独报错:
func compute() (value float64, ok bool) {
// ...
if failed {
return 0, false
}
return result, true
}
3.3 复数
complex64/128分别由float32/64构成。由内置的complex()函数创建;内置的real()和imag()函数分别提取实部和虚部。 在源码中,如果一个数字紧接一个i如:-1.32i
就表示一个实部为0的虚数。
3.4 布尔值
ture或false。 这里的布尔运算是包括'逻辑短路'的。 ture和false不能用数字1、0等表示。
func btoi(b bool) int {
if b {
return 1
}
return 0
}
3.5 字符串
string
被解读成按UTF8编码在Unicode码点。(utf8储存) UTF-8是变长编码,长度在1-4字节
func main() {
var fit string = "你好,世界!"
fmt.Println(fit) // 你好,世界!
fmt.Println(fit[0], fit[1], fit[2]) // 228(E4) 189(BD) 160(A0)
fmt.Println(fit[0:3]) // 你
fmt.Println(len(fit)) // 14
}
可见,s[i]操作访问的是字节而不是字符,len(s)返回字节数而不是字符数。 如果i不在0\leqslant i\leqslant len(s)
会触发宕机异常。
字符串可以追加、重复值,但是不可以改变字符串包含本身序列的值。
func main() {
var fit string = "Hello,world!"
s := fit + "HELLO!!!"
fmt.Println(s) // Hello,world!HELLO!!!
fit[0] = 'E' // cannot assign to fit[0] (neither addressable nor a map index expression)
}
不可变意味着两个字符串能安全地共用同一段底层内存,使得复制任何长度字符串的开销都低廉。类似的,字符串s及其子串(如s[:7])可以安全地共用数据,因此子串生成操作的开销低廉。这两种情况都没有重新分配内存。![]()
3.5.1 字符串字面量
字符串的值可以直接写成带引号的字符串字面量(string literal)形式。 由于go默认使用utf-8编码,所以我们可以轻易地插入转义字符,如:\a
、\r
、\n
、\"
、\
等。 当然也可以用16或8进制表示(都表示单字节):
- 16进制:
\x
开头,必须两位16进制数表示(大小不敏感)。\xAc
- 8进制:
\o
开头,必须三位8进制数表示,不可超过\377
。\120
原生的字符串字面量的书写形式是
,使用反引号而不是双引号。 原生的字符串字面量内,转义序列不起作用;实质内容与字面写法严格一致,包括
...
\
和 \n
。 因此在程序源码中,原生的字符串字面量可以展开多行。唯一的特殊处理是回车符会被删除换行符会保留。这使得同一字符串所在所有平台上的值相同。包括习惯在文本文件存入换行符的系统。
正则表达式往往含有大量 \
,可以方便地写成原生的字符串字面量,原生的字面量也适用于HTML模板、JSON字面量、命令行提示信息以及需要多行表达场景。
const GoUsage = `Go is a tool for managing Go source code.
Usage:
go command [arguments]
...`
3.5.2 Unicode
从前,事情简单明晰,至少,狭隘的看,软件处理只需一个字符集:ASCII。
ASCII是用7bit(占用1B)表示的字符。但当今兼顾多语言与效率的是Unicode(unicode.org)。在go中使用rune (int32)
表示。
但仍为ASCII‘当道’的环境下,Unicode会导致资源浪费。怎么改进呢?
神说要有 \'低浪费的编码\' ,于是便有了 \'UTF-8\' 。
3.5.3 UTF-8
一种Unicode标准,每个字符采用1~4B表示,变长低占用。(编码规则见参考链接)
0xxxxxxx runes 0-127 (ASCII)
110xxxxx 10xxxxxx 128-2047 (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
UTF-8编码可自同步,最多追溯3字节,所以可以定位起始位置。同时作为前置编码可以左→右解码而不产生歧义;也无需超前阅读。
像GBK之类的编码,如果不知道起点位置则可能会出现歧义)。没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。 ————由GO语言圣经译者注
unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等),unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。
在go中可以使用转义来轻松使用Unicode码点(原本很难指明);分别使用\uhhhh
和\Uhhhhhhhh
来表示,每一个h代表以为16进制数。(后者很少用) (编码规则见参考链接)
此外还有一些有意思的点见参考链接“字符串 - UTF-8 - Go语言圣经”。
由go的两位创始人Ken Thompson和Rob Pike发明。
3.5.4 字符串和字节slice
四个重要的包:
- strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
- bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。
- strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
- unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。
func main() {
s := "abc"
c1 := []byte(s)
c2 := string(c1)
c1[0] = 'f'
c2[0] = 'f'
}
概念上,c1 := []byte(s)
会生成一个新的字节数组,拷贝s含有的字节并返回指向整个数组的slice引用。少数情况下会优化成不复制,但为保证s不可变一般不会优化
反之,c2 := string(c1)
也会生成副本,保证c2不可变。
3.5.5 字符串和数字的转换
strconv:提供了各种类型和对应字符串的相互转换。 如要将整数转string,其一选用fmt.Sprintf,另一种就是strconv.Itoa (a是ascii)
x := 123
fmt.Println(strconv.Itoa(x)) // 123
FormatInt和FormatUint函数可以用不同的进制来格式化数字:
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
// FormatInt(i int64, base int) string
// FormatInt returns the string representation of i in the given base, for 2 <= base <= 36. The result uses the lower-case letters 'a' to 'z' for digit values >= 10.
// 就是说第一个数是int,第二个是数制。
fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要包含有附加额外信息的时候。 如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数:
x, err := strconv.Atoi("123") // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits 64是数制
3.6 常量
常量是一种表达式,都是基本类型(boolean、string、数字),防止出现运行时意外(或恶意)修改。
const pi = 3.1415926
const (
e = 2.71818
tes = 3.1571224 / 2.3
)
某些操作要在运行时才能检测到(÷0、下标越界等),但如果是常量则可以编译时报错。
常量相关的操作返回的也是常量,如:常量的数学、逻辑、比较运算,常量的类型转换和某些内置函数返回值(len、cap、complex等)。
常量声明会自行推断类型,如
const noDelay time.Duration = 0
和
const timeout = 5 * time.Minute
。由于time.Minute底层依赖于int64,所以两者都是time.Duration类型常量。
若同时声明多组变量,则除第一行外后面可省。后面复用第一行。
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // 1 1 2 2
3.6.1 常量生成器iota
iota从0取值,每次加1。
const (
sunday Weekday = iota // 0
monday // 1
tuesday // 2
wednesday // 3
thursday // 4
friday // 5
saturday // 6
)
也可以存在
const ( Flagup Flags = 1 << iota )形式
。
以及更复杂的形式
const (
_ = 1 << (10 * iota)
kib // 1024
)
但iota也存在局限:如因为不存在指数运算符不能生成更为人知的1000的幂。
3.6.2 无类型常量
一个常量可以有任意一个如int或float64或类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。无类型的常量:
六种未明确类型的常量类型:无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
- 可以通过延迟明确具体类型
- 可以提供更高的运算精度
- 可以不使用显式类型转换(前提是转换合理)
fmt.Println(YiB/ZiB) // "1024"
另一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方(对应3):
此时z已经成为comlex128
const z complex128 = math.Pi
const f float64 = float64(z)
对于常量面值,不同的写法可能会对应不同的类型(0和0.0,0i的区别)。同样,true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。
前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果:
var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // "100"; (f - 32) * 5 is a float64
fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float
如果转换合法的话,无类型的常量将会被隐式转换为对应的类型。无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。对于浮点数和复数,可能会有舍入处理(这里或许是由无类型的高精度导致?):
const (
deadbeef = 0xdeadbeef // untyped int with value 3735928559
a = uint32(deadbeef) // uint32 with value 3735928559
b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
c = float64(deadbeef) // float64 with value 3735928559 (exact)
d = int32(deadbeef) // compile error: constant overflows int32
e = float64(1e309) // compile error: constant overflows float64
f = uint(-1) // compile error: constant underflows uint
)
对于一个没有显式类型的变量声明(包括简短变量声明),由常量决定默认类型。
注意有一点不同:无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。如果不知道浮点数类型的内存大小是很难写出正确的数值算法的,因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型。
当尝试将这些无类型的常量转为一个接口值时(见第7章),这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。
fmt.Printf("%T\n", 0) // "int"
fmt.Printf("%T\n", 0.0) // "float64"
fmt.Printf("%T\n", 0i) // "complex128"
fmt.Printf("%T\n", '\000') // "int32" (rune)