第三章讲解了整数、浮点数、复数、布尔类型、字符串以及常量

3.1 整型

有符号整数类型

int int8 int16 int32 int64
32bits/64bits 8bits 16bits 32bits 64bits

无符号整数类型

uint uint8 uint16 uint32 uint64
32bits/64bits 8bits 16bits 32bits 64bits
  • 其中int和int32也是不同的类型,即使int的大小也是32bit,在需要将int当作int32类型的地方需要一个显式的类型转换操作,反之亦然
  • Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。
    • 这两个名称可以互换使用。
  • byte是uint8类型的等价类型
    • byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。
  • 无符号的整数类型uintptr,没有指定具体的bit大小但是足以容纳指针
    • uintptr类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。
    • 不管它们的具体大小,int、uint和uintptr是不同类型的兄弟类型。

二进制表示

  • 有符号整数采用2的补码形式表示,也就是最高bit位用来表示符号位
    • 一个n-bit的有符号数的值域是从\(-2^{n-1}\)\(2^{n-1}-1\)
  • 无符号整数的所有bit位都用于表示非负数
    • 值域是0到\(2^n-1\)
  • 例如,int8类型整数的值域是从-128到127,而uint8类型整数的值域是从0到255。

算术运算、逻辑运算和比较运算的二元运算符

按照优先级递减的顺序排列

  • 二元运算符有五种优先级。
  • 在同一个优先级,使用左优先结合规则
  • 但是使用括号可以明确优先顺序,使用括号也可以用于提升优先级

取模号和除号的特殊处

  • 在Go语言中,%取模运算符的符号和被取模数的符号总是一致的
    • 因此-5%3-5%-3结果都是-2。
  • 除法运算符/的行为则依赖于操作数是否全为整数
    • 比如5.0/4.0的结果是1.25,但是5/4的结果是1,因为整数除法会向着0方向截断余数。

比较表达式

比较表达式的结果是布尔类型。

1
2
3
4
5
6
==    等于
!= 不等于
< 小于
<= 小于等于
> 大于
>= 大于等于

bit位操作运算符

前面4个操作运算符并不区分是有符号还是无符号数

1
2
3
4
5
6
&      位运算 AND
| 位运算 OR
^ 位运算 XOR
&^ 位清空(AND NOT)
<< 左移
>> 右移
  • 位操作运算符^作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反,也就是返回一个每个bit位都取反的数

  • 位操作运算符&^用于按位置零(AND NOT)

    • 如果对应y中bit位为1的话,表达式z = x &^ y结果z的对应的bit位为0,否则z对应的bit位等于x相应的bit位的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var x uint8 = 1<<1 | 1<<5
var y uint8 = 1<<1 | 1<<2

fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
fmt.Printf("%08b\n", y) // "00000110", the set {1, 2}

fmt.Printf("%08b\n", x&y) // "00000010", the intersection {1}
fmt.Printf("%08b\n", x|y) // "00100110", the union {1, 2, 5}
fmt.Printf("%08b\n", x^y) // "00100100", the symmetric difference {2, 5}
fmt.Printf("%08b\n", x&^y) // "00100000", the difference {5}

for i := uint(0); i < 8; i++ {
if x&(1<<i) != 0 { // membership test
fmt.Println(i) // "1", "5"
}
}

fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6}
fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}
  • x<<nx>>n移位运算中,决定了移位操作的bit数部分必须是无符号数;被操作的x可以是有符号数或无符号数

  • 算术上,一个x<<n左移运算等价于乘以\(2^n\),一个x>>n右移运算等价于除以\(2^n\)

  • 左移运算用零填充右边空缺的bit位,无符号数的右移运算也是用0填充左边空缺的bit位,

  • 但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。

  • 因为这个原因,最好用无符号运算,这样你可以将整数完全当作一个bit位模式处理。

类型转换操作

1
2
3
var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // compile error

当尝试编译这三个语句时,将产生一个错误信息:

1
invalid operation: apples + oranges (mismatched types int32 and int16)

常见方法

1
var compote = int(apples) + int(oranges)
  • 如果转换允许的话,类型转换操作T(x)将x转换为T类型。
  • 许多整数之间的相互转换并不会改变数值;它们只是告诉编译器如何解释这个值。
  • 但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度

八进制或十六进制

  • 任何大小的整数字面值都可以用以0开始的八进制格式书写,例如0666;
  • 或用以0x或0X开头的十六进制格式书写,例如0xdeadbeef
    • 十六进制数字可以用大写或小写字母。
  • 如今八进制数据通常用于POSIX操作系统上的文件访问权限标志,十六进制数字则更强调数字值的bit位模式。
1
2
3
4
5
6
o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
  • fmt的两个使用技巧
    • 通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操作数,但是%之后的[1]副词告诉Printf函数再次使用第一个操作数
    • 第二,%后的#副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀。

字符使用%c参数打印,或者是用%q参数打印带单引号的字符

1
2
3
4
5
6
ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"
fmt.Printf("%d %[1]q\n", newline) // "10 '\n'"

3.2. 浮点数

  • Go语言提供了两种精度的浮点数
    • float32和float64

浮点数的极值

  • 浮点数的范围极限值可以在math包找到
    • 常量math.MaxFloat32表示float32能表示的最大数值,大约是 3.4e38
    • 对应的math.MaxFloat64常量大约是1.8e308。
    • 它们分别能表示的最小值近似为1.4e-45和4.9e-324。

浮点数的精度

  • 一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度
  • 通常应该优先使用float64类型
    • 因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大
    • 因为float32的有效bit位只有23个,其它的bit位用于指数和符号;当整数大于23bit能表达的范围时,float32的表示将出现误差
1
2
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1) // "true"!

小数

  • 浮点数的字面值可以直接写小数部分
1
const e = 2.71828 // (approximately)
  • 小数点前面或后面的数字都可能被省略(例如.707或1.)
    • 很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分
1
2
const Avogadro = 6.02214129e23  // 阿伏伽德罗常数
const Planck = 6.62606957e-34 // 普朗克常数

打印

  • 用Printf函数的%g参数打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度
  • 对应表格的数据,使用%e(带指数)%f的形式打印可能更合适
1
2
3
for x := 0; x < 8; x++ {
fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}
1
2
3
4
5
6
7
8
x = 0       e^x =    1.000
x = 1 e^x = 2.718
x = 2 e^x = 7.389
x = 3 e^x = 20.086
x = 4 e^x = 54.598
x = 5 e^x = 148.413
x = 6 e^x = 403.429
x = 7 e^x = 1096.633

正无穷大和负无穷大

  • 正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;

  • 还有NaN非数,一般用于表示无效的除法操作结果0/0或Sqrt(-1).

  • 函数math.IsNaN用于测试一个数是否是非数NaN,math.NaN则返回非数对应的值。

    • 虽然可以用math.NaN来表示一个非法的结果,但是测试一个结果是否是非数NaN则是充满风险的,因为NaN和任何数都是不相等的
    • 在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的bit模式表示
1
2
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
  • 如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败
1
2
3
4
5
6
7
func compute() (value float64, ok bool) {
// ...
if failed {
return 0, false
}
return result, true
}

3.3. 复数

  • Go语言提供了两种精度的复数类型:
    • complex64和complex128
    • 分别对应float32和float64两种浮点数精度

构建复数

  • 内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部
1
2
3
4
5
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"
  • 如果一个浮点数面值或一个十进制整数面值后面跟着一个i,它将构成一个复数的虚部,复数的实部是0
    • 例如3.141592i或2i
1
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
  • x和y的声明语句还可以简化(用自然方法书写)
1
2
x := 1 + 2i
y := 3 + 4i

复数的比较

  • 复数也可以用==和!=进行相等比较。
    • 只有两个复数的实部和虚部都相等的时候它们才是相等的

复数的处理方法

  • math/cmplx包提供了复数处理的许多函数,例如求复数的平方根函数和求幂函数。
1
fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"

3.4. 布尔型

  • 一个布尔类型的值只有两种:true和false。

  • if和for语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。

  • 一元操作符!对应逻辑非操作,因此!true的值为false

和、或

  • 布尔值可以和&&(AND)和||(OR)操作符结合
    • 并且有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值
1
s ![] == 'x'
  • &&的优先级比||

  • 布尔值并不会隐式转换为数字值0或1反之亦然。必须使用一个显式的if语句辅助转换

1
2
3
4
i := 0
if b {
i = 1
}
  • 包装成一个函数
1
2
3
4
5
6
7
// btoi returns 1 if b is true and 0 if false.
func btoi(b bool) int {
if b {
return 1
}
return 0
}

数字到布尔型的逆转换

1
2
// itob reports whether i is non-zero.
func itob(i int) bool { return i != 0 }

3.5. 字符串

  • 一个字符串是一个不可改变的字节序列

  • 字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。

  • 文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列

字符串长度和索引

  • 内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目)
  • 索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。
1
2
3
s := "hello, world"
fmt.Println(len(s)) // "12"
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
  • 试图访问超出字符串索引范围的字节将会导致panic异常
1
c := s[len(s)] // panic: index out of range

字节和字符

  • 第i个字节并不一定是字符串的第i个字符
    • 因为对于非ASCII字符的UTF8编码会要两个或多个字节
  • 子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串
    • 生成的新字符串将包含j-i个字节
1
fmt.Println(s[0:5]) // "hello"

索引起始和终结

不管i还是j都可能被忽略,当它们被忽略时将采用0作为开始位置,采用len(s)作为结束的位置。

1
2
3
fmt.Println(s[:5]) // "hello"
fmt.Println(s[7:]) // "world"
fmt.Println(s[:]) // "hello, world"

拼接、比较字符串

  • +操作符将两个字符串连接构造一个新字符串
1
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
  • 字符串可以用==和<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。

字符串的不变形

字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值

1
2
3
s := "left foot"
t := s
s += ", right foot"

这并不会导致原始的字符串值被改变,但是变量s将因为+=语句持有一个新的字符串值,但是t依然是包含原先的字符串值。

1
2
fmt.Println(s) // "left foot, right foot"
fmt.Println(t) // "left foot"

因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的

1
s[0] = 'L' // compile error: cannot assign to s[0]

不变性意味着如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。

3.5.1. 字符串面值

  • 字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号内即可
  • 因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。

转义字符

在一个双引号包含的字符串面值中,可以用以反斜杠\开头的转义序列插入任意的数据。

1
2
3
4
5
6
7
8
9
10
\a      响铃
\b 退格
\f 换页
\n 换行
\r 回车
\t 制表符
\v 垂直制表符
\' 单引号(只用在 '\'' 形式的rune符号面值中)
\" 双引号(只用在 "..." 形式的字符串面值中)
\\ 反斜杠
  • 通过十六进制或八进制转义在字符串面值中包含任意的字节。
    • 一个十六进制的转义形式是\xhh,其中两个h表示十六进制数字(大写或小写都可以)。
    • 一个八进制转义形式是\ooo,包含三个八进制的o数字(0到7),但是不能超过\377
      • 对应一个字节的范围,十进制为255
    • 每一个单一的字节表达一个特定的值。

原生的字符串

  • 一个原生的字符串面值形式是,使用反引号代替双引号。
  • 在原生的字符串面值中,没有转义操作
  • 全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行
    • 在原生字符串面值内部是无法直接写`字符的,可以用八进制或十六进制转义或+"`"连接字符串常量完成
  • 唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的,包括那些把回车也放入文本文件的系统
    • Windows系统会把回车和换行一起放入文本文件中
  • 原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠
  • 原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。
1
2
3
4
5
const GoUsage = `Go is a tool for managing Go source code.

Usage:
go command [arguments]
...`

3.5.2. Unicode

ASCII字符集:美国信息交换标准代码。

  • ASCII,更准确地说是美国的ASCII
  • 使用7bit来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设备控制符。

Unicode

  • 收集了这个世界上所有的符号系统,

    • 包括重音符号和其它变音符号,制表符和回车符
    • 还有很多神秘的符号
  • 每个符号都分配一个唯一的Unicode码点

  • Unicode码点对应Go语言中的rune整数类型(rune是int32等价类型)

  • 通用的表示一个Unicode码点的数据类型是int32,也就是Go语言中rune对应的类型

  • 将一个符文序列表示为一个int32序列。

    • 这种编码方式叫UTF-32或UCS-4,每个Unicode码点都使用同样大小的32bit来表示
    • 这种方式比较简单统一,但是它会浪费很多存储空间
      • 因为大多数计算机可读的文本是ASCII字符,本来每个ASCII字符只需要8bit或1字节就能表示
      • 而且即使是常用的字符也远少于65,536个,也就是说用16bit编码方式就能表达常用字符。

3.5.3. UTF-8

  • UTF8是一个将Unicode码点编码为字节序列的变长编码
  • UTF8编码使用1到4个字节来表示每个Unicode码点
    • ASCII部分字符只使用1个字节
    • 常用字符部分使用2或3个字节表示。
  • 每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。
    • 如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容
    • 如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。
    • 更大的Unicode码点也是采用类似的策略处理。
1
2
3
4
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)

优点

  • 变长的编码无法直接通过索引来访问第n个字符,但是UTF8编码获得了很多额外的优点。
    1. UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步
      • 可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置
    2. 也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看
      • 像GBK之类的编码,如果不知道起点位置则可能会出现歧义
    3. 没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串
      • 搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰
    4. UTF8编码的顺序和Unicode码点的顺序一致
      • 可以直接排序UTF8编码序列
    5. 没有嵌入的NUL(0)字节
      • 很好地兼容那些使用NUL作为字符串结尾的编程语言。

Go语言中的UTF8

  • Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色
    • unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等)
    • unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能

有很多Unicode字符很难直接从键盘输入,并且还有很多字符有着相似的结构;有一些甚至是不可见的字符

  • Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式
    • \uhhhh对应16bit的码点值
    • \Uhhhhhhhh对应32bit的码点值
      • 其中h是一个十六进制数字
      • 一般很少需要使用32bit的形式。
      • 每一个对应码点的UTF8编码。
1
2
3
4
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"

上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。

Unicode转义也可以使用在rune字符中。下面三个字符是等价的:

1
'世' '\u4e16' '\U00004e16'
  • 对于小于256的码点值可以写在一个十六进制转义字节中

    • 例如\x41对应字符'A'
  • 但是对于更大的码点则必须使用\u\U转义形式

  • 因此,\xe4\xb8\x96并不是一个合法的rune字符,虽然这三个字节对应一个有效的UTF8编码的码点

  • 得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作

例子:查看字节数

字符串包含13个字节,以UTF8形式编码,但是只对应9个Unicode字符

1
2
3
4
5
import "unicode/utf8"

s := "Hello, 世界"
fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"

为了处理这些真实的字符,我们需要一个UTF8解码器。unicode/utf8包提供了该功能,我们可以这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
i += size
fmt.Printf("%d\t%c\n", i, r)
}
1 H
2 e
3 l
4 l
5 o
6 ,
7
10
13
  • 每一次调用DecodeRuneInString函数都返回一个r和长度,r对应字符本身,长度对应r采用UTF8编码后的编码字节数目。
  • 长度可以用于更新第i个字符在字符串中的字节索引位置。

简洁写法

1
2
3
4
5
6
7
8
9
10
11
12
13
for i, r := range "Hello, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}

0 'H' 72
1 'e' 101
2 'l' 108
3 'l' 108
4 'o' 111
5 ',' 44
6 ' ' 32
7 '世' 19990
10 '界' 30028

统计字符数

1
2
3
4
5
6
7
n := 0
for _, _ = range s {
n++
}
for range s {
n++
}
  • 每一个UTF8字符解码,不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码,

  • 如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符\uFFFD

    • 在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。

    • 当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串

  • UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。

  • 将[]rune类型转换应用到UTF8编码的字符串,将返回字符串编码的Unicode码点序列

1
2
3
4
5
// "program" in Japanese katakana
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"

在第一个Printf中的% x参数用于在每个十六进制数字前插入一个空格。

  • 如果是将一个[]rune类型的Unicode字符slice或数组转为string,则对它们进行UTF8编码:
1
fmt.Println(string(r)) // "プログラム"
  • 将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串
1
2
fmt.Println(string(65))     // "A", not "65"
fmt.Println(string(0x4eac)) // "京"
  • 如果对应码点的字符是无效的,则用\uFFFD无效字符作为替换
1
fmt.Println(string(1234567)) // "?"

3.5.4. 字符串和Byte切片

标准库中有四个包对字符串处理尤为重要

  • bytes
  • strings
  • strconv
  • unicode

4个包的功能

  • strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
    • ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。
  • bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型
    • 因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制
    • 这种情况下,使用bytes.Buffer类型将会更有效
  • strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
  • unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类
    • 每个函数有一个单一的rune类型的参数,然后返回一个布尔值。
    • 像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。
    • 所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。

实现basename

basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除

1
2
3
fmt.Println(basename("a/b/c.go")) // "c"
fmt.Println(basename("c.d.go")) // "c.d"
fmt.Println(basename("abc")) // "abc"
手工实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c
func basename(s string) string {
// Discard last '/' and everything before.
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '/' {
s = s[i+1:]
break
}
}
// Preserve everything before last '.'.
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '.' {
s = s[:i]
break
}
}
return s
}
使用strings.LastIndex库函数
1
2
3
4
5
6
7
8
func basename(s string) string {
slash := strings.LastIndex(s, "/") // -1 if "/" not found
s = s[slash+1:]
if dot := strings.LastIndex(s, "."); dot >= 0 {
s = s[:dot]
}
return s
}

实现数字分隔

函数的功能是将一个表示整数值的字符串,每隔三个字符插入一个逗号分隔符,例如“12345”处理后成为“12,345”。

1
2
3
4
5
6
7
8
// comma inserts commas in a non-negative decimal integer string.
func comma(s string) string {
n := len(s)
if n <= 3 {
return s
}
return comma(s[:n-3]) + "," + s[n-3:]
}
  • 输入comma函数的参数是一个字符串。如果输入字符串的长度小于或等于3的话,则不需要插入逗号分隔符。
  • 否则,comma函数将在最后三个字符前的位置将字符串切割为两个子串并插入逗号分隔符,然后通过递归调用自身来得出前面的子串。

字符串和字节slice

  • 一个字符串是包含只读字节的数组,一旦创建,是不可变的。
  • 相比之下,一个字节slice的元素则可以自由地修改。

字符串和字节slice之间可以相互转换

1
2
3
s := "abc"
b := []byte(s)
s2 := string(b)
  • 从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组
  • 编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在变量b被修改的情况下,原始的s字符串也不会改变
  • 将一个字节slice转换到字符串的string(b)操作则是构造一个字符串拷贝,以确保s2字符串是只读的。

为了避免转换中不必要的内存分配,bytes包和strings同时提供了许多实用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// strings包
func Contains(s, substr string) bool
func Count(s, sep string) int
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string

// bytes包
func Contains(b, subslice []byte) bool
func Count(s, sep []byte) int
func Fields(s []byte) [][]byte
func HasPrefix(s, prefix []byte) bool
func Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte
  • bytes包还提供了Buffer类型用于字节slice的缓存
    • 一个Buffer开始是空的,但是随着string、byte或[]byte等类型数据的写入可以动态增长
    • 一个bytes.Buffer变量并不需要初始化,因为零值也是有效的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// intsToString is like fmt.Sprint(values) but adds commas.
func intsToString(values []int) string {
var buf bytes.Buffer
buf.WriteByte('[')
for i, v := range values {
if i > 0 {
buf.WriteString(", ")
}
fmt.Fprintf(&buf, "%d", v)
}
buf.WriteByte(']')
return buf.String()
}

func main() {
fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]"
}

当向bytes.Buffer添加任意字符的UTF8编码时,最好使用bytes.Buffer的WriteRune方法,但是WriteByte方法对于写入类似'['和']'等ASCII字符则会更加有效。

练习 3.10: 编写一个非递归版本的comma函数,使用bytes.Buffer代替字符串链接操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func comma(s string) string {
if len(s) <= 3 {
fmt.Println(s)
return s
}

var res bytes.Buffer
var n uint8 = 0
for i := len(s) - 1; i >= 0; i-- {
res.WriteByte(s[i])
n++
if n == 3 && i != 0{
res.WriteString(",")
n = 0
}
}

r := res.String()
var ans bytes.Buffer
for i := len(r) - 1; i >= 0; i-- {
ans.WriteByte(r[i])
}
fmt.Println(ans.String())
return ans.String()
}

练习 3.11: 完善comma函数,以支持浮点数处理和一个可选的正负号的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// comma inserts commas in a non-negative decimal integer string.
func comma(s string) string {
var flag bool
if s[0] == '+' || s[0] == '-' {
flag = true
}
n := len(s)

for i := 0; i < n; i++ {
if s[i] == '.' && flag {
return string(s[0]) + comma(s[1:i]) + s[i:]
} else if s[i] == '.' {
return comma(s[:i]) + s[i:]
}
}

if n <= 3 {
return s
}
return comma(s[:n-3]) + "," + s[n-3:]
}

练习 3.12: 编写一个函数,判断两个字符串是否是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func isSame(s1, s2 string) bool {
if len(s1) != len(s2) {
return false
}

r1 := make(map[byte]int)
r2 := make(map[byte]int)

for i := 0; i < len(s1); i++ {
r1[s1[i]]++
r2[s2[i]]++
}

for c, i := range r1 {
if r2[c] != i {
return false
}
}
return true
}

3.5.5. 字符串和数字的转换

  • 由strconv包提供这类转换功能。

整数转为字符串

  • 将一个整数转为字符串,
    • 一种方法是用fmt.Sprintf返回一个格式化的字符串;
    • 另一个方法是用strconv.Itoa(“整数到ASCII”)
1
2
3
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"
  • FormatInt和FormatUint函数可以用不同的进制来格式化数字
1
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
  • fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要包含有附加额外信息的时候
1
s := fmt.Sprintf("x=%b", x) // "x=1111011"

字符串转为整数

可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数

1
2
x, err := strconv.Atoi("123")             // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bit
  • ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。

  • 在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。

  • 有时候也会使用fmt.Scanf来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。

3.6. 常量

  • 常量表达式的值在编译期计算,而不是在运行期。

  • 每种常量的潜在类型都是基础类型:boolean、string或数字。

一个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改

1
const pi = 3.14159 // approximately; math.Pi is a better approximation

批量声明

1
2
3
4
const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
  • 所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。

  • 当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

  • 常量间的所有算术运算、逻辑运算和比较运算的结果也是常量

  • 对常量的类型转换操作或以下函数调用都是返回常量结果:

    • len、cap、real、imag、complex和unsafe.Sizeof
  • 因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分

1
2
3
4
5
6
7
const IPv4Len = 4

// parseIPv4 parses an IPv4 address (d.d.d.d).
func parseIPv4(s string) IP {
var p [IPv4Len]byte
// ...
}
  • 一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型
1
2
3
4
5
const noDelay time.Duration = 0
const timeout = 5 * time.Minute
fmt.Printf("%T %[1]v\n", noDelay) // "time.Duration 0"
fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s"
fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"
  • 如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略
  • 如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的
1
2
3
4
5
6
7
8
const (
a = 1
b
c = 2
d
)

fmt.Println(a, b, c, d) // "1 1 2 2"

3.6.1. iota 常量生成器

  • 常量声明可以使用iota常量生成器初始化

  • 它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式

  • 在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

  • 类似于枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
type Weekday int

const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
// 周日将对应0,周一为1,如此等等。

也可以在复杂的常量表达式中使用iota

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Flags uint

const (
FlagUp Flags = 1 << iota // is up
FlagBroadcast // supports broadcast access capability
FlagLoopback // is a loopback interface
FlagPointToPoint // belongs to a point-to-point link
FlagMulticast // supports multicast access capability
)

func IsUp(v Flags) bool { return v&FlagUp == FlagUp }
func TurnDown(v *Flags) { *v &^= FlagUp }
func SetBroadcast(v *Flags) { *v |= FlagBroadcast }
func IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 }

func main() {
var v Flags = FlagMulticast | FlagUp
fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true"
TurnDown(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false"
SetBroadcast(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 false"
fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true"
}
1
2
3
4
5
6
7
8
9
10
11
const (
_ = 1 << (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (exceeds 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (exceeds 1 << 64)
YiB // 1208925819614629174706176
)

不过iota常量生成规则也有其局限性。例如,它并不能用于产生1000的幂(KB、MB等),因为Go语言并没有计算幂的运算符

练习 3.13: 编写KB、MB的常量声明,然后扩展到YB。

1
2
3
4
5
6
7
8
9
10
11
const (
B = 1 << (3 + 10 * iota)
KB
MB
GB
TB
PB
EB
ZB
YB
)

3.6.2. 无类型常量

  • Go语言的常量有个不同寻常之处
    • 虽然一个常量可以有任意一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型
    • 但是许多常量并没有一个明确的基础类型。
    • 编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;
      • 你可以认为至少有256bit的运算精度。
    • 这里有六种未明确类型的常量类型,分别是
      • 无类型的布尔型
      • 无类型的整数
      • 无类型的字符
      • 无类型的浮点数
      • 无类型的复数
      • 无类型的字符串
  • 通过延迟明确常量的具体类型
    • 无类型的常量不仅可以提供更高的运算精度
    • 而且可以直接用于更多的表达式而不需要显式的类型转换
1
fmt.Println(YiB/ZiB) // "1024"

一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方

1
2
3
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

如果math.Pi被确定为特定类型,比如float64,那么结果精度可能会不一样,同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换

1
2
3
4
5
const Pi64 float64 = math.Pi

var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)

除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果

1
2
3
4
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
  • 只有常量可以是无类型的。
    • 当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,
    • 或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,
    • 无类型的常量将会被隐式转换为对应的类型,如果转换合法的话
1
2
3
4
var f float64 = 3 + 0i // untyped complex -> float64
f = 2 // untyped integer -> float64
f = 1e123 // untyped floating-point -> float64
f = 'a' // untyped rune -> float64

上面的语句相当于:

1
2
3
4
var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a')
  • 无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。
  • 对于浮点数和复数,可能会有舍入处理
1
2
3
4
5
6
7
8
9
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
)
  • 对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子
1
2
3
4
i := 0      // untyped integer;        implicit int(0)
r := '\000' // untyped rune; implicit rune('\000')
f := 0.0 // untyped floating-point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)
  • 注意有一点不同:无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128

  • 如果不知道浮点数类型的内存大小是很难写出正确的数值算法的,因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型

如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型

1
2
var i = int8(0)
var i int8 = 0

当尝试将这些无类型的常量转为一个接口值时,这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。

1
2
3
4
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)