mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-08-17 20:01:48 +00:00
转为 mdbook
This commit is contained in:
384
ch3/ch3-05.md
384
ch3/ch3-05.md
@@ -66,12 +66,386 @@ s[0] = 'L' // compile error: cannot assign to s[0]
|
||||
不变性意味着如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。同样,一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个子串共享相同的底层数据。
|
||||
|
||||
|
||||
{% include "./ch3-05-1.md" %}
|
||||
### 3.5.1. 字符串面值
|
||||
|
||||
{% include "./ch3-05-2.md" %}
|
||||
字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号内即可:
|
||||
|
||||
{% include "./ch3-05-3.md" %}
|
||||
```
|
||||
"Hello, 世界"
|
||||
```
|
||||
|
||||

|
||||
|
||||
因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。
|
||||
|
||||
在一个双引号包含的字符串面值中,可以用以反斜杠`\`开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式:
|
||||
|
||||
```
|
||||
\a 响铃
|
||||
\b 退格
|
||||
\f 换页
|
||||
\n 换行
|
||||
\r 回车
|
||||
\t 制表符
|
||||
\v 垂直制表符
|
||||
\' 单引号(只用在 '\'' 形式的rune符号面值中)
|
||||
\" 双引号(只用在 "..." 形式的字符串面值中)
|
||||
\\ 反斜杠
|
||||
```
|
||||
|
||||
可以通过十六进制或八进制转义在字符串面值中包含任意的字节。一个十六进制的转义形式是`\xhh`,其中两个h表示十六进制数字(大写或小写都可以)。一个八进制转义形式是`\ooo`,包含三个八进制的o数字(0到7),但是不能超过`\377`(译注:对应一个字节的范围,十进制为255)。每一个单一的字节表达一个特定的值。稍后我们将看到如何将一个Unicode码点写到字符串面值中。
|
||||
|
||||
一个原生的字符串面值形式是\`...\`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写\`字符的,可以用八进制或十六进制转义或+"\`"连接字符串常量完成)。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的,包括那些把回车也放入文本文件的系统(译注:Windows系统会把回车和换行一起放入文本文件中)。
|
||||
|
||||
原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。
|
||||
|
||||
```Go
|
||||
const GoUsage = `Go is a tool for managing Go source code.
|
||||
|
||||
Usage:
|
||||
go command [arguments]
|
||||
...`
|
||||
```
|
||||
|
||||
### 3.5.2. Unicode
|
||||
|
||||
在很久以前,世界还是比较简单的,起码计算机世界就只有一个ASCII字符集:美国信息交换标准代码。ASCII,更准确地说是美国的ASCII,使用7bit来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设备控制符。对于早期的计算机程序来说,这些就足够了,但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。随着互联网的发展,混合多种语言的数据变得很常见(译注:比如本身的英文原文或中文翻译都包含了ASCII、中文、日文等多种语言字符)。如何有效处理这些包含了各种语言的丰富多样的文本数据呢?
|
||||
|
||||
答案就是使用Unicode( http://unicode.org ),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,每个符号都分配一个唯一的Unicode码点,Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型)。
|
||||
|
||||
在第八版本的Unicode标准里收集了超过120,000个字符,涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢?通用的表示一个Unicode码点的数据类型是int32,也就是Go语言中rune对应的类型;它的同义词rune符文正是这个意思。
|
||||
|
||||
我们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4,每个Unicode码点都使用同样大小的32bit来表示。这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是ASCII字符,本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个,也就是说用16bit编码方式就能表达常用字符。但是,还有其它更好的编码方法吗?
|
||||
|
||||
### 3.5.3. UTF-8
|
||||
|
||||
UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码是由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。
|
||||
|
||||
```
|
||||
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编码获得了很多额外的优点。首先UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步:它可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看(译注:像GBK之类的编码,如果不知道起点位置则可能会出现歧义)。没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。
|
||||
|
||||
Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等),unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。
|
||||
|
||||
有很多Unicode字符很难直接从键盘输入,并且还有很多字符有着相似的结构;有一些甚至是不可见的字符(译注:中文和日文就有很多相似但不同的字)。Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式:`\uhhhh`对应16bit的码点值,`\Uhhhhhhhh`对应32bit的码点值,其中h是一个十六进制数字;一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。例如:下面的字母串面值都表示相同的值:
|
||||
|
||||
```
|
||||
"世界"
|
||||
"\xe4\xb8\x96\xe7\x95\x8c"
|
||||
"\u4e16\u754c"
|
||||
"\U00004e16\U0000754c"
|
||||
```
|
||||
|
||||
上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。
|
||||
|
||||
Unicode转义也可以使用在rune字符中。下面三个字符是等价的:
|
||||
|
||||
```
|
||||
'世' '\u4e16' '\U00004e16'
|
||||
```
|
||||
|
||||
对于小于256的码点值可以写在一个十六进制转义字节中,例如`\x41`对应字符'A',但是对于更大的码点则必须使用`\u`或`\U`转义形式。因此,`\xe4\xb8\x96`并不是一个合法的rune字符,虽然这三个字节对应一个有效的UTF8编码的码点。
|
||||
|
||||
得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀:
|
||||
|
||||
```Go
|
||||
func HasPrefix(s, prefix string) bool {
|
||||
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
||||
}
|
||||
```
|
||||
|
||||
或者是后缀测试:
|
||||
|
||||
```Go
|
||||
func HasSuffix(s, suffix string) bool {
|
||||
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
|
||||
}
|
||||
```
|
||||
|
||||
或者是包含子串测试:
|
||||
|
||||
```Go
|
||||
func Contains(s, substr string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if HasPrefix(s[i:], substr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。(上面的函数都来自strings字符串处理包,真实的代码包含了一个用哈希技术优化的Contains 实现。)
|
||||
|
||||
另一方面,如果我们真的关心每个Unicode字符,我们可以使用其它处理方式。考虑前面的第一个例子中的字符串,它混合了中西两种字符。图3.5展示了它的内存表示形式。字符串包含13个字节,以UTF8形式编码,但是只对应9个Unicode字符:
|
||||
|
||||
```Go
|
||||
import "unicode/utf8"
|
||||
|
||||
s := "Hello, 世界"
|
||||
fmt.Println(len(s)) // "13"
|
||||
fmt.Println(utf8.RuneCountInString(s)) // "9"
|
||||
```
|
||||
|
||||
为了处理这些真实的字符,我们需要一个UTF8解码器。unicode/utf8包提供了该功能,我们可以这样使用:
|
||||
|
||||
```Go
|
||||
for i := 0; i < len(s); {
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
fmt.Printf("%d\t%c\n", i, r)
|
||||
i += size
|
||||
}
|
||||
```
|
||||
|
||||
每一次调用DecodeRuneInString函数都返回一个r和长度,r对应字符本身,长度对应r采用UTF8编码后的编码字节数目。长度可以用于更新第i个字符在字符串中的字节索引位置。但是这种编码方式是笨拙的,我们需要更简洁的语法。幸运的是,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。下面的循环运行如图3.5所示;需要注意的是对于非ASCII,索引更新的步长将超过1个字节。
|
||||
|
||||

|
||||
|
||||
```Go
|
||||
for i, r := range "Hello, 世界" {
|
||||
fmt.Printf("%d\t%q\t%d\n", i, r, r)
|
||||
}
|
||||
```
|
||||
|
||||
我们可以使用一个简单的循环来统计字符串中字符的数目,像这样:
|
||||
|
||||
```Go
|
||||
n := 0
|
||||
for _, _ = range s {
|
||||
n++
|
||||
}
|
||||
```
|
||||
|
||||
像其它形式的循环那样,我们也可以忽略不需要的变量:
|
||||
|
||||
```Go
|
||||
n := 0
|
||||
for range s {
|
||||
n++
|
||||
}
|
||||
```
|
||||
|
||||
或者我们可以直接调用utf8.RuneCountInString(s)函数。
|
||||
|
||||
正如我们前面提到的,文本字符串采用UTF8编码只是一种惯例,但是对于循环的真正字符串并不是一个惯例,这是正确的。如果用于循环的字符串只是一个普通的二进制数据,或者是含有错误编码的UTF8数据,将会发生什么呢?
|
||||
|
||||
每一个UTF8字符解码,不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码,如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符`\uFFFD`,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串。
|
||||
|
||||
UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。
|
||||
|
||||
将[]rune类型转换应用到UTF8编码的字符串,将返回字符串编码的Unicode码点序列:
|
||||
|
||||
```Go
|
||||
// "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编码:
|
||||
|
||||
```Go
|
||||
fmt.Println(string(r)) // "プログラム"
|
||||
```
|
||||
|
||||
将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串:
|
||||
|
||||
```Go
|
||||
fmt.Println(string(65)) // "A", not "65"
|
||||
fmt.Println(string(0x4eac)) // "京"
|
||||
```
|
||||
|
||||
如果对应码点的字符是无效的,则用`\uFFFD`无效字符作为替换:
|
||||
|
||||
```Go
|
||||
fmt.Println(string(1234567)) // "?"
|
||||
```
|
||||
|
||||
### 3.5.4. 字符串和Byte切片
|
||||
|
||||
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
|
||||
|
||||
bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。
|
||||
|
||||
strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
|
||||
|
||||
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。
|
||||
|
||||
下面例子的basename函数灵感源于Unix shell的同名工具。在我们实现的版本中,basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除:
|
||||
|
||||
```Go
|
||||
fmt.Println(basename("a/b/c.go")) // "c"
|
||||
fmt.Println(basename("c.d.go")) // "c.d"
|
||||
fmt.Println(basename("abc")) // "abc"
|
||||
```
|
||||
|
||||
第一个版本并没有使用任何库,全部手工硬编码实现:
|
||||
|
||||
<u><i>gopl.io/ch3/basename1</i></u>
|
||||
```Go
|
||||
// basename removes directory components and a .suffix.
|
||||
// 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库函数:
|
||||
|
||||
<u><i>gopl.io/ch3/basename2</i></u>
|
||||
```Go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
path和path/filepath包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作。斜杠本身不应该用于文件名,但是在其他一些领域可能会用于文件名,例如URL路径组件。相比之下,path/filepath包则使用操作系统本身的路径规则,例如POSIX系统使用/foo/bar,而Microsoft Windows使用`c:\foo\bar`等。
|
||||
|
||||
让我们继续另一个字符串的例子。函数的功能是将一个表示整数值的字符串,每隔三个字符插入一个逗号分隔符,例如“12345”处理后成为“12,345”。这个版本只适用于整数类型;支持浮点数类型的留作练习。
|
||||
|
||||
<u><i>gopl.io/ch3/comma</i></u>
|
||||
```Go
|
||||
// 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之间可以相互转换:
|
||||
|
||||
```Go
|
||||
s := "abc"
|
||||
b := []byte(s)
|
||||
s2 := string(b)
|
||||
```
|
||||
|
||||
从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在变量b被修改的情况下,原始的s字符串也不会改变。将一个字节slice转换到字符串的string(b)操作则是构造一个字符串拷贝,以确保s2字符串是只读的。
|
||||
|
||||
为了避免转换中不必要的内存分配,bytes包和strings同时提供了许多实用函数。下面是strings包中的六个函数:
|
||||
|
||||
```Go
|
||||
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包中也对应的六个函数:
|
||||
|
||||
```Go
|
||||
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
|
||||
```
|
||||
|
||||
它们之间唯一的区别是字符串类型参数被替换成了字节slice类型的参数。
|
||||
|
||||
bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的,但是随着string、byte或[]byte等类型数据的写入可以动态增长,一个bytes.Buffer变量并不需要初始化,因为零值也是有效的:
|
||||
|
||||
<u><i>gopl.io/ch3/printints</i></u>
|
||||
```Go
|
||||
// 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字符则会更加有效。
|
||||
|
||||
bytes.Buffer类型有着很多实用的功能,我们在第七章讨论接口时将会涉及到,我们将看看如何将它用作一个I/O的输入和输出对象,例如当做Fprintf的io.Writer输出对象,或者当作io.Reader类型的输入源对象。
|
||||
|
||||
**练习 3.10:** 编写一个非递归版本的comma函数,使用bytes.Buffer代替字符串链接操作。
|
||||
|
||||
**练习 3.11:** 完善comma函数,以支持浮点数处理和一个可选的正负号的处理。
|
||||
|
||||
**练习 3.12:** 编写一个函数,判断两个字符串是否是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。
|
||||
|
||||
### 3.5.5. 字符串和数字的转换
|
||||
|
||||
除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见。由strconv包提供这类转换功能。
|
||||
|
||||
将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv.Itoa(“整数到ASCII”):
|
||||
|
||||
```Go
|
||||
x := 123
|
||||
y := fmt.Sprintf("%d", x)
|
||||
fmt.Println(y, strconv.Itoa(x)) // "123 123"
|
||||
```
|
||||
|
||||
FormatInt和FormatUint函数可以用不同的进制来格式化数字:
|
||||
|
||||
```Go
|
||||
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
|
||||
```
|
||||
|
||||
fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要包含有附加额外信息的时候:
|
||||
|
||||
```Go
|
||||
s := fmt.Sprintf("x=%b", x) // "x=1111011"
|
||||
```
|
||||
|
||||
如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数:
|
||||
|
||||
```Go
|
||||
x, err := strconv.Atoi("123") // x is an int
|
||||
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
|
||||
```
|
||||
|
||||
ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。
|
||||
|
||||
有时候也会使用fmt.Scanf来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。
|
||||
|
||||
{% include "./ch3-05-4.md" %}
|
||||
|
||||
{% include "./ch3-05-5.md" %}
|
||||
|
178
ch3/ch3-06.md
178
ch3/ch3-06.md
@@ -58,9 +58,183 @@ fmt.Println(a, b, c, d) // "1 1 2 2"
|
||||
|
||||
如果只是简单地复制右边的常量表达式,其实并没有太实用的价值。但是它可以带来其它的特性,那就是iota常量生成器语法。
|
||||
|
||||
{% include "./ch3-06-1.md" %}
|
||||
### 3.6.1. iota 常量生成器
|
||||
|
||||
{% include "./ch3-06-2.md" %}
|
||||
常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。
|
||||
|
||||
下面是来自time包的例子,它首先定义了一个Weekday命名类型,然后为一周的每天定义了一个常量,从周日0开始。在其它编程语言中,这种类型一般被称为枚举类型。
|
||||
|
||||
```Go
|
||||
type Weekday int
|
||||
|
||||
const (
|
||||
Sunday Weekday = iota
|
||||
Monday
|
||||
Tuesday
|
||||
Wednesday
|
||||
Thursday
|
||||
Friday
|
||||
Saturday
|
||||
)
|
||||
```
|
||||
|
||||
周日将对应0,周一为1,如此等等。
|
||||
|
||||
我们也可以在复杂的常量表达式中使用iota,下面是来自net包的例子,用于给一个无符号整数的最低5bit的每个bit指定一个名字:
|
||||
|
||||
```Go
|
||||
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
|
||||
)
|
||||
```
|
||||
|
||||
随着iota的递增,每个常量对应表达式1 << iota,是连续的2的幂,分别对应一个bit位置。使用这些常量可以用于测试、设置或清除对应的bit位的值:
|
||||
|
||||
<u><i>gopl.io/ch3/netflag</i></u>
|
||||
```Go
|
||||
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"
|
||||
}
|
||||
```
|
||||
|
||||
下面是一个更复杂的例子,每个常量都是1024的幂:
|
||||
|
||||
```Go
|
||||
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。
|
||||
|
||||
### 3.6.2. 无类型常量
|
||||
|
||||
Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
|
||||
|
||||
通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。例如,例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围,但是它们依然是合法的常量,而且像下面的常量表达式依然有效(译注:YiB/ZiB是在编译期计算出来的,并且结果常量是1024,是Go语言int变量能有效表示的):
|
||||
|
||||
```Go
|
||||
fmt.Println(YiB/ZiB) // "1024"
|
||||
```
|
||||
|
||||
另一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:
|
||||
|
||||
```Go
|
||||
var x float32 = math.Pi
|
||||
var y float64 = math.Pi
|
||||
var z complex128 = math.Pi
|
||||
```
|
||||
|
||||
如果math.Pi被确定为特定类型,比如float64,那么结果精度可能会不一样,同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换:
|
||||
|
||||
```Go
|
||||
const Pi64 float64 = math.Pi
|
||||
|
||||
var x float32 = float32(Pi64)
|
||||
var y float64 = Pi64
|
||||
var z complex128 = complex128(Pi64)
|
||||
```
|
||||
|
||||
对于常量面值,不同的写法可能会对应不同的类型。例如0、0.0、0i和`\u0000`虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。
|
||||
|
||||
前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果:
|
||||
|
||||
```Go
|
||||
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
|
||||
```
|
||||
|
||||
只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。
|
||||
|
||||
```Go
|
||||
var f float64 = 3 + 0i // untyped complex -> float64
|
||||
f = 2 // untyped integer -> float64
|
||||
f = 1e123 // untyped floating-point -> float64
|
||||
f = 'a' // untyped rune -> float64
|
||||
```
|
||||
|
||||
上面的语句相当于:
|
||||
|
||||
```Go
|
||||
var f float64 = float64(3 + 0i)
|
||||
f = float64(2)
|
||||
f = float64(1e123)
|
||||
f = float64('a')
|
||||
```
|
||||
|
||||
无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。对于浮点数和复数,可能会有舍入处理:
|
||||
|
||||
```Go
|
||||
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
|
||||
)
|
||||
```
|
||||
|
||||
对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子:
|
||||
|
||||
```Go
|
||||
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语言不存在整型类似的不确定内存大小的浮点数和复数类型。
|
||||
|
||||
|
||||
如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型,像下面例子这样:
|
||||
|
||||
```Go
|
||||
var i = int8(0)
|
||||
var i int8 = 0
|
||||
```
|
||||
|
||||
当尝试将这些无类型的常量转为一个接口值时(见第7章),这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。
|
||||
|
||||
```Go
|
||||
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)
|
||||
```
|
||||
|
||||
现在我们已经讲述了Go语言中全部的基础数据类型。下一步将演示如何用基础数据类型组合成数组或结构体等复杂数据类型,然后构建用于解决实际编程问题的数据结构,这将是第四章的讨论主题。
|
||||
|
||||
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# 第三章 基础数据类型
|
||||
# 第3章 基础数据类型
|
||||
|
||||
虽然从底层而言,所有的数据都是由比特组成,但计算机一般操作的是固定大小的数,如整数、浮点数、比特数组、内存地址等。进一步将这些数组织在一起,就可表达更多的对象,例如数据包、像素点、诗歌,甚至其他任何对象。Go语言提供了丰富的数据组织形式,这依赖于Go语言内置的数据类型。这些内置的数据类型,兼顾了硬件的特性和表达复杂数据结构的便捷性。
|
||||
|
||||
|
Reference in New Issue
Block a user