Go chapter2


实数

声明浮点类型变量

每个变量都有与之相关联的类型,其中声明和初始化实数变量就需要用到浮点类型。

以下代码具有相同的作用,即使我们不为days变量指定类型,go编译器也会根据给定值推断出该变量的类型

1
2
3
days :=365.2425
var days=265.2425
var days float64=365.2425

在go语言中,所有带小数点的数字在默认情况下都会被设置为float64类型

如果使用整数去初始化一个变量,匿名go语言只有在显式地指定浮点类型的情况下,才会将其声明为浮点类型变量

1
var days float64=30

单精度浮点型

go 语言拥有两种浮点类型,其中默认的浮点类型为 float64,每个 64 位的浮点数需要占用 8 字节内存,很多语言都使用术语双精度浮点数来描述这种浮点数。

go语言的另一个浮点类型是 float32 类型,又称单精度浮点数,它占用的内存只有 float64 的一半,但它提供的精度不如 float64 高。为了使用 float32 浮点数,必须在声明变量时指定变量类型。

1
2
3
4
5
6
7
8
//使用math包中定义的值
var p64=math.Pi
var p32 float64=math.Pi
fmt.Println(p64)
fmt.Println(p32)
//运行结果
3.141592653589793
3.1415927

math 包中函数处理的都是 float64 类型的值,所以除非你有特殊理由,否则就应该优先使用 float64 类型。

零值

在go语言中,每种类型都有相应的默认值,我们将其称为零值(zero value)。

当你声明一个变量但是却没有为它设置初始值的时候,该变量就会被初始化为零值。

1
2
3
4
5
6
var price flaot64
//运行结果,打印出数字0
fmt.Println(price)

//对于计算机来说,上述声明与以下声明是完全相同的
price:=0.0

打印浮点类型

在使用 print 或者 println 处理浮点类型的时候,函数默认将打印出尽可能多的小数位数。如果这并不是你想要的效果,那么可以通过 printf 函数的格式化变量 %f 来指定被打印小数的位数。

1
2
3
4
5
6
third := 1.0/3
fmt.Println(third)//打印出0.3333333333333333
fmt.Printf("%v\n",third)//打印出0.3333333333333333
fmt.Printf("%f\n",third)//打印出333333
fmt.Printf("%.3f\n",third)//打印出0.333
fmt.Printf("%4.2f\n",third)//打印出0.33

格式化变量 %f 将根据给定的宽度和精度格式化 third 变量的值。

格式化变量的精度用于指定小数点之后应该出现的数字数量。

1
2
宽度 精度
"%4.2f"

另外,格式化变量的宽度指定了打印整个实数(包括整数部分、小数部分和小数点在内)需要显示的最小字符数量。如果用户给定的宽度比打印实数所需的字符数量要大,那么 printf 将使用空格填充输出的左侧。在用户没有指定宽度的情况下,printf 将按需调整打印实数所需的字符数量。

如果想使用数字 0 而不是空格来填充输出的左侧,那么只需要像如下所示的那样,在宽度的前面加上一个 0 即可。

1
2
fmt.Println("%05.f\n",third)
//打印出00.33

浮点精确性

正如 0.33 只是 1/3 的近似值一样,在数学上,某些有理数是无法用小数形式表示的。那么自然地,对近似值即使也将产生一个近似结果。

例如:

1
2
1/3+1/3+1/3=1
0.33+0.33+0.33=0.99

因为计算机硬件使用只包含 0 和 1 的二进制数而不是包含 0~9 的十进制来表示浮点数,所以浮点数经常会受到舍入错误的影响。
例如:

1
2
3
4
third := 1.0/3
fmt.Printf("%f\n",third)//打印出0.333333
fmt.Printf("%7.4f\n",third)//打印出0.3333
fmt.Printf("%06.2f\n",third)//打印出000.33

计算机虽然可以精确地表示 1/3,但是在使用这个数字和其它数字进行计算的时候却会引发舍入错误。

1
2
3
4
5
6
7
third := 1.0/3.0
fmt.Println(third+third+third)//打印出1

piggyBank := 0.1
piggyBank += 0.2
fmt.Println(piggyBank)
//打印出 0.30000000000000004

正如所见,浮点数并不是准确无误地,不适合存储对精度要求很高的数字。

我们可以让 printf 函数只打印小数点后两位小数,这样就可以把底层实参导致的舍入错误掩盖掉。

为了尽可能地减少舍入错误,我们还可以将乘法计算放到除法计算的前面执行,这种做法通常会得出更为精确的计算结果。

先执行除法运算

1
2
3
4
cel := 21.0
fmt.Print((cel/5.0*9.0)+32,"F\n")
fmt.Print((9.0/5.0*cel)+32,"F\n")
//都打印出69.80000000000001F

先执行乘法计算

1
2
3
4
cel := 21.0
fah := (cel*9.0/5.0)+32.0
fmt.Print(fah,"F")
//打印出69.8F

避免舍入错误的最佳方法是不使用浮点数

比较浮点数

注意,在代码清单中,piggyBank变量的值是 0.30000000000000004 而不是我们想要的 0.30。在比较浮点数的时候,必须小心。

1
2
3
4
5
piggyBank := 0.1
piggyBank += 0.2
//piggyBank值:0.30000000000000004
fmt.Println(piggyBank==0.3)
//结果为false

为了避免上述问题,我们可以另辟蹊径,不直接比较两个浮点数,而计算出它们之间的差,然后通过判断这个差的绝对值是否足够小来判断两个浮点数是否相等。为此,我们可以使用 math 包提供的 Abs 函数来计算 float64 浮点数的绝对值:

1
fmt.Println(math.Abs(piggyBank-0.3)<0.0001)//打印出true

整数

声明整数类型变量

在go提供的众多整数类型当中,有 5 种整数类型是有符号(signed) 的,这意味着它们既可以表示正整数,又可以表示负整数。在这些整数类型中,最常用的莫过于代表有符号整数的 int 类型了:

1
var year int=2018

除有符号整数之外,go还提供了 5 种只能表示非负整数的无符号(unsigned) 整数类型,其中的典型为 uint 类型:

1
var month uint=2

因为go在进行类型推断的时候总是会选择 int 类型作为整数值的类型,所以下面这3行代码的意义是完全相同的:

1
2
3
year := 2018
var year = 2018
var year int = 2018

提示,如果类型推断可以正确的为变量设置类型,那么我们就没有必要为其指定 int 类型

为不同场合而设的整数类型

无论是有符号整数还是无符号整数,它们都有各种不同大小(size)的类型可供选择,而不同大小又会影响它们自身的取值范围以及内存占用。

下表列出了8种与计算机架构无关的整数类型,以及这些类型需要占用的内存大小。

类型 取值范围 内存占用情况
int8 -128~127 8位(1字节)
uint8 0~255 8位(1字节)
int16 -32 768~32 767 16位(2字节)
uint16 0~65 535 16位(2字节)
int32 -2 147 483 648~2 147 483 647 32位(4字节)
uint32 0~4 294 967 295 32位(4字节)
int64 -9 223 372 036 854 775 808~9 223 372 036 854 775 807 64位(8字节)
uint64 0~18 446 744 073 709 551 615 64位(8字节)
int 类型和 uint 类型会根据目标硬件选择最合适的位长,所以它们未被包含在表里。

提示:如果你的程序需要操作20亿以上的数值并且可能会在32位架构上运行,那么请确保你使用的是 int64 类型或者 uint64 类型,而不是 int 类型或者 uint 类型

注意:在某些架构上把 int 看作 int32,而在另一些架构上则把 int 看作 int64,这是一种非常想当然的想法,但这种想法实际上并不正确:int 不是其它任何类型的别名,int、int32 和 int64 实际上是3种不同的类型。

了解类型

如果你对go编译器推断的类型感到好奇,那么可以使用 printf 函数提供的格式化变量 %T 去查看指定变量的类型。

1
2
3
4
5
year:=2018
fmt.Printf("value:%v\ntype: %T\n",year,year)
//运行结果
value:2018
type: int

为了避免在 printf 函数中重复使用同一个变量两次,我们可以将[1]添加到第二个格式化变量 %v 中,以此来复用第一个格式化变量的值,从而避免代码重复:

1
2
3
4
5
year:=2018
fmt.Printf("value:%v\ntype: %[1]T\n",year)
//运行结果
value:2018
type: int

为8位颜色使用uint8类型

层叠样式表(CSS)技术通过范围为 0255 的红绿蓝三原色来指定画面上的颜色。因为 8 位无符号整数正好可以表示范围为 0255 的值,所以使用 uint8 类型来表示层叠样式表中的颜色可以说是再合适不过了。

1
var read,green,blue uint8=0,141,213

与常见的 int 类型相比,使用 uint8 类型有以下好处。

  • uint8 类型可以将变量的值限制在合法范围之内,与 32 位整数相比,uint8 消除了超过40 亿种可能出现的错误。
  • 对于未压缩图片这种需要按顺序存储大量颜色的场景,使用 8 位整数可以节省大量内存空间。

go语言中的十六进制数字
在go语言中表示十六进制数字必须带有 0x 前缀
使用 Printf 函数打印十六进制数字,可以使用 %x 作为格式化变量

整数回绕

在go语言中,当超过整数类型的取值范围时,就会出现整数环绕现象。

1
2
3
4
5
6
7
8
9
var red uint8 = 255
red++
fmt.Println(red)
//打印出0

var number int8 = 127
number++
fmt.Println(number)
//打印出-128

聚焦二进制位

为了了解整数出现环绕的原因,我们需要将注意力放到二进制位上,为此需要用到格式化变量 %b,它可以以二进制的形式打印出相应的整数值。跟其他格式化变量一样,%b 也可以启用零填充功能并指定格式化输出的最小长度。

对 green 加 1 导致进位,最终计算得出二进制数 00000100,也就是十进制数4。

1
2
3
4
5
6
var green uint8 = 3
fmt.Printf("%08b\n",green)
//打印出00000011
green++
fmt.Printf("%08b\n",green)
//打印出00000100

提示:math 包定义了值为 65535 的常量 math.MaxUint16,还有与架构无关的整数类型的最大值常量以及最小值常量。再次提醒一下,由于 int 类型和 uint 类型的位长在不同硬件上可能会有所不同,因此 math 包并没有定义这两种类型的最大值常量和最小值常量。

在对值为 255 的 8 位无符号整数 blue 执行增量运算的时候,同样的进位操作将再次出现,但这次进位跟前一次进位有一个重要的区别:对只有 8 位的变量 blue 来说,最高位的 1 将 “无处容身”,并导致变量的值变为0。

1
2
3
4
5
6
var blue uint8=255
fmt.Printf("%08b\n",blue)
//11111111
blue++
fmt.Printf("%08b\n",blue)
//00000000

虽然回绕在某些情况下可能正好是你想要获得的状态,但是有时候也会成为问题。最简单的避免回绕的方法就是选用一种足够长的整数类型,使它能够容纳你想要存储的值。

避免时间环绕

基于 Unix 的操作系统都是由协调时间时(UTC)1970年 1月 1日以来的秒数来表示时间,但是这个秒数在 2038 年将超过 20 亿,也就是大致相当于 int32 类型的最大值。

虽然 32 位整数无法存储 2038 年以后的日期,但是我们可以通过 64 位整数来解决。在任何平台上,使用 int64 类型和 uint64 类型都可以轻而易举地存储大于 20 亿的数字。

如下代码使用了一个超过 120 亿的巨大值来展示go足以应对 2038 年后的日期。这段代码使用了来自 time 包的 Unix 函数,该函数接受两个 int64 类型的值作为参数,它们分别代表协调世界时 1970 年 1 月 1 日 以来的秒数和纳秒数。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"time"
)
func main(){
future :=time.Unix(12622780800,0)
fmt.Println(future)
}
//运行结果
2370-01-01 08:01:20 +0800 CST

大数

击中天花板

利用变量存储半人马座阿尔法星与地球之间的距离——41.3万亿公里。

这样的大数是无法使用 int32 类型和 uint32 类型存储的。但使用 int64 类型存储这样的值却是绰绰有余的。

go语言可以通过使用指数来减少键入 0 的次数

1
2
3
var dis int64=41300000000000
//使用指数形式
var dis int64=41.3e12

尽管 64 位整数已经非常大了,但与整个宇宙相比,它们还是有些太渺小了。具体来说,即使是最大的无符号数类型 uint64,它能存储的数值上限也仅为 18 艾(10的18次方)。

对存储更大的值,比如地球和仙女星系之间的距离 24 艾来说,尝试使用 uint64 类型存储这一距离将引发溢出错误。

虽然uint64无法处理这种非常大的数值,但是我们还有其他选择。例如,前面介绍过的浮点数。

除了浮点类型之外,我们还有另一种方法,那就是接下来要介绍的big包

注意:如果用户没有显示地为包含指数的数值变量指定类型,那么GO将推断其类型为float64

big包

big包提供了以下 3 种类型

  • 存储大整数的 big.Int,它可以轻而易举地存储超过 18 艾的数字。
  • 存储任意精度浮点数的 big.Float。
  • 存储诸如 1/3 的分数的 big.Rat。

注意:除了使用现有的类型,用户还可以自行声明新类型。

虽然地球与仙女星系之间的距离足有 24 艾公里,但对 big.Int 类型来说,这不过一个微小不足道的数值,big.Int 完全有能力存储和操作它。

一但决定使用 big.Int,就需要在等式的每个部分都使用这种类型,即使对已存在的常量来说也是如此。使用 big.Int 类型最基本的方法就是使用 NewInt 函数,该函数接受一个 int64 类型的值作为输入,返回一个 big.Int 类型的值作为输出:

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
"math/big"
)
func main(){
num :=big.NewInt(299792)
fmt.Println(num)
}

NewInt 虽然使用起来非常方便,但是它对创建 24 艾这种超过 int64 取值上限的大数来说并无帮助。为此,我们可以通过给定一个 string 来创建相应的 big.Int 类型的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"math/big"
)
func main(){
num :=big.NewInt(299792)
dis :=new(big.Int)
dis.SetString("240000000000000000",10)
fmt.Println(num)
fmt.Println(dis)
}
//运行结果
299792
240000000000000000

这段代码在创建 big.Int 变量之后,会通过调用 SetString 方法来将它的值设置为 24 艾。另外,因为数值 24 艾是基于十进制的,所以传给SetString 方法的第二个参数为 10。

注意:方法跟函数非常相似

像 big.Int 这样的大类型虽然能够精确表示任意大小的数值,但代价是使用起来比 int、float64 等原生类型要麻烦,并且运行速度也会相对较慢。

大小非同寻常的常量

常量声明可以跟变量声明一样带有类型,但是常量也无法用 uint64 类型存储像 24 艾这样的巨大值:

1
2
const dis uint64 = 2400000000000000000
//尝试定义一个值为2400000000000000000的常量将导致 uint64 类型溢出

但是,如果声明的是一个不带类型的常量,那么事情就会变得有趣起来。正如之前所述,如果在声明整数类型变量的时候没有显示地为其指定类型,那么 Go 将通过类型推断为其指定 int 类型,而当变量的值为 24 艾时,这一行为将导致 int 类型溢出。然而 go 语言在处理常量时的做法与处理常量时的做法并不相同。具体来说go语言不会为常量推断类型,而是直接将其标识为无类型(untyped)。例如,以下代码就不会引发溢出错误:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
)
func main(){
const dis=240000000000000000
fmt.Println(dis)
}
//运行结果
240000000000000000

常量通过关键字 const 进行声明,除此之外,程序里的每个字面值(literal value)也都是常量。这意味着那些大小非同寻常的数值可以被直接使用,就像如下代码。

1
2
3
fmt.Println(240000000000000000/299792/86400)
//运行结果
9265683

针对常量和字面量的计算将在编译时而不是程序运行时执行。因为go的编译器就是用go语言编写的,并且在底层实现中,无类型的数值常量将由big包提供支持,所以程序能够直接对超过18艾的数值常量执行所有常规运算。

变量也可以使用常量作为值,只要变量的大小能够容纳容量即可。例如,虽然 int 类型的变量无法容纳24艾,但让它存储 926 568 346还是没有任何问题的:

1
2
3
const dis=926568346
km:=dis
fmt.Println(km)

使用大小非同寻常的常量有一个需要注意的地方:尽管go编译器使用 big 包处理无类型的数值常量,但常量与 big.Int 值是无法互换的。

非常大的常量虽然很有用,但它们还是无法完全取代 big 包。

多语言文本

声明字符串变量

因为go语言会把用双引号包围的字面值推断为 string 类型,所以以下3行代码的作用是相同的:

1
2
3
peace := "peace"
var peace = "peace"
var peace string = "peace"

如果你声明了一个变量但是没有为它赋值,那么go语言将使用变量类型的零值对其进行初始化,而string类型的零值就是空字符串""

1
var blank string

原始字符串字面量

字符串字面量可以包含转义字符,如果你想要的是字符\n本身而不是一个新的文本行,那么你可以像如下所示的那样,使用反引号(`
)而不是双引号(”)来包围文本。使用反引号包围的字符串被称为原始字符串字面量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
)
func main(){
fmt.Println("1\n2\n3")
fmt.Println(`1\n2\n3`)
}
//运行结果
1
2
3
1\n2\n3

跟普通字符串字面量不同的是,原始字符串字面量可以在代码里面跨越多个文本行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
)

func main(){
fmt.Println(`
1
2
3`)
}
//运行结果
//字符串中用于缩进的制表符也被正确地打印了出来

1
2
3

无论是字符串字面量还是原始字符串字面量,最终都将变成字符串。

字符、代码点、符文和字节

统一码联盟(Unicode Consortium)把名为代码点的一系列数值赋值给了上百万个独一无二的字符。

go语言提供了 rune(符文) 类型用于表示单个统一码代码点,该类型是 int32 类型的别名。

除此之外,go语言还提供了 uint8 类型的别名 byte,这种类型既可以表示二进制数据,又可以表示由美国信息交换标准代码(ASCII)定义的英文字符(历史悠久的ASCII包含128个字符,它是统一码的子集)。

类型别名
因为类型别名实际上就是同一类型的不同名字,所以 rune 和 int32 是可以互换的。尽管 byte 和 rune 从一开始就出现在了go里面,但是从go 1.9开始,用户也可以自行声明类型别名,就像这样:
type byte uint8
type rune = int32

在 Printf 中使用格式化变量%c,可以打印单个字符。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)

func main(){
var pi rune = 960
fmt.Printf("%c\n",pi)
}
//运行结果
π

提示:虽然任意一种整数类型都可以使用格式化变量%c,但是通过使用别名 rune 可以表明数字90代表字符而不是数字。

为了免除用户记忆统一码代码点的烦恼,go提供了相应的字符字面量句法。用户只需要像 'A' 这样使用单引号将字符包围起来,就可以取得该字符的代码点。如果用户声明了一个字符变量却没有为其指定类型,那么go将推断该变量的类型为 rune,因此下面代码是等效的

1
2
3
grade := 'A'
var grade = 'A'
var grade rune='A'

提示:虽然 rune 类型代表的是一个字符,但它实际存储的仍然是数字值,因此 grade 变量存储的仍然是大写字母 ‘A’ 的代码点,也就是数字65。除 rune 之外,字符字面量也可以搭配别名 byte 一同使用:

1
var star byte ='*'

拉弦

我们可以将不同字符串赋值给同一个变量,但是无法对字符本身进行修改:

1
2
peace := "shalom"
peace = "salam"

与此类似,我们的程序虽然可以独立访问字符串中的单个字符,但是不能修改这些字符。

下列代码展示了如何通过方括号[]指定指向字符串的索引,从而达到访问指定 ASCII 字符的目的。字符串索引以 0 为起始值。

通过索引获取字符串中的指定字符

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)
func main(){
message := "shalom"
c:=message[5]
fmt.Printf("%c\n",c)
}
//运行结果
m

Ruby 中的字符串和 C 中的字符数组允许被修改,而go中的字符串与 python、java 和 javascript 中的字符串一样,都是不可变的,你不能修改go中的字符串:

1
2
//结果会报错
message[5]='d'

使用凯撒加密法处理字符

凯撒密码:对字符进行位移加密

如下

1
2
3
4
5
c:='a'
c=c+3
fmt.Printf("%c\n",c)
//运行结果
d

如上代码展示的方法并不完美,因为它没有考虑该如何处理字符 ‘x’、’y’、’z’,所以它无法对 xylophones、yaks 和 zebras 这样的单词实施加密。为了解决这个问题,最初的凯撒加密法采取了回绕措施,也就是将 ‘x’ 变为 ‘a’、’y’变为’b’,而 ‘z’ 则变为 ‘c’。对于包含 26 个字符的英文字母表,我们可以通过这段代码实现上述变换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)
func main(){
c:='a'
c=c+3
if c>'z'{
c=c-26
}
fmt.Printf("%c\n",c)
}
//运行结果
d

凯撒密码的解密方法跟加密方法正好相反,程序不再是为字符加上 3 而是减去 3,并且它还需要在字符过小也就是 c<’a’ 的时候,将字符加上26 以实施回绕。虽然上述的加密方法和解密方法都非常直观,但由于它们都需要处理字符边界以实现回绕,因此实际的编码过程将变得相当痛苦。

凯撒解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import(
"fmt"
)

func main(){
c:='d'
c=c-3
if c<'a'{
c=c+26
}
fmt.Printf("%c\n",c)
}

现代变体

回转13(ROT13)是凯撒密码在 20 世纪的一个变体,该变体跟凯撒密码的唯一区别就在于,它给字符添加的量是 13 而不是 3,并且ROT13的加密和解密可以通过同一个方法实现,这是非常方便的。

现在,假设搜寻地外文明计划(Searchfor Extra-terrestrial Intelligence,SETI)的相关机构在外太空扫描外星人通信信息的时候,发现了包含以下消息的广播:

1
message :="uv vagreangvbany fcnpr fgngvba"

我们有预感,这条消息很可能是使用 ROT13 加密的英文文本,但是在解密这条消息之前,我们还需要知悉其包含的字符数量,这可以通过内置的len函数来确定:

1
fmt.Println(len(message))//打印出30

注意:go拥有少量无须导入语句即可使用的内置函数,len函数就是其中之一,它可以测定各种不同类型的值的长度。

ROT13消息解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
)

func main(){
message :="uv vagreangvbany fcnpr fgngvba"
for i:=0;i<len(message);i++{
c:=message[i]
if c>='a'&&c<='z'{
c=c+13
if c>'z'{
c=c-26
}
}
fmt.Printf("%c",c)
}
}
//运行结果
hi international space station

将字符串解码为符文

有好几种可以为统一代码点编码,而go中的字符串使用的 UTF-8 编码就是其中的一种。UTF-8 是一种高效的可变程度的编码方式,它可以用8个、16个或者32个二进制位为单个代码点编码。在可变长度编码方式的基础上,UTF-8 沿用了 ASCII 字符的编码,从而使得 ASCII 字符可以直接转换为相应的 UTF-8 编码字符。

上面代码展示的 ROT13 程序只会单独访问 message 字符串的每个字节(8位),但是没有考虑到各个字符可能会由多个字节组成。因此这个程序只能处理英文字符,但是无法处理其他字符。不过这个问题并不难解决。

为了让 ROT13 能够支持多种语言,程序首先要做的就是在处理字符之前先将它们解码为 rune 类型。幸运的是,go正好提供了解码 UTF-8 的字符串所需的函数和语言特性。

utf8 包提供了实现上述想法所需的两个函数,而 其中uneCountInString 函数能够以符文而不是以字节为单位返回字符串的长度,而 DecodeRuneInString 函数则能够解码字符串的首个字符并返回解码后的符文占用的字节数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"unicode/utf8"
)
func main(){
question:="¿Cómo estás?"
fmt.Println(len(question),"bytes")
fmt.Println(utf8.RuneCountInString(question),"runes")
c,size:=utf8.DecodeRuneInString(question)
fmt.Printf("%c %v\n",c,size)
}
//运行结果
15 bytes
12 runes
¿ 2

注意:go跟很多编程语言不同的一点在于,go允许返回多个值

正如以下代码所示,go语言提供的关键字range不仅可以迭代各种不同的收集器,它还可以 utf-8 解码。

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
package main

import (
"fmt"
)

func main(){
question:="¿Cómo estás?"
for i,c:=range question{
fmt.Printf("%v %c\n",i,c)
}
}
//运算结果
0 ¿
2 C
3 ó
5 m
6 o
7
8 e
9 s
10 t
11 á
13 s
14 ?

在每次迭代中,变量i都会被赋值为字符串的当前索引,而变量c则会被赋值为该索引上的代码点。

如果你不需要在迭代的时候获取索引,那么只要使用go的空白表示符_(下划线)来省略它即可:

1
2
3
4
5
for _,c:= range question{
fmt.Printf("%c",c)
}
//运行结果
¿Cómo estás?

类型转换

类型不能混合使用

变量的类型决定了它能够执行的操作,例如,数值类型可以执行加法运算,而字符串类型则可以执行拼接操作,诸如此类。通过加法操作符,可以将两个字符串拼接在一起:

1
countdown := "Launch in T minus"+"10 seconds" 

但是,如果尝试拼接数值和字符串,那么编译器就会报错。

尝试混合使用整数类型和浮点类型同样会引发类型不匹配错误。在Go中,整数将被推断为整数类型,而诸如 365.2425 这样的实数则会被表示为浮点类型。

1
2
3
4
5
6
7
//以下两个变量都是整数类型
age:=41
marsDays:=687
//以下变量为浮点类型
earthDays:=365.2425
//以下操作会报错,因为类型不匹配
fmt.Println("I am",age*earthDays/marsDays,"years old on Mars.")

数字类型转换

类型转换的用法非常简短。举个例子,如果你想把整数类型变量 age 转换为浮点类型以执行计算,那么只需要使用与新类型同名的函数来包裹该变量即可:

1
2
age := 41
marsAge := float64(age)

虽然go语言不允许混合使用不同类型的变量,但是通过类型转换,如下代码中的计算将会顺利进行。

1
2
3
4
5
age := 41
marsAge := float64(age)
marsDays := 687.0
earthDays := 365.2425
fmt.Println("I am",marsAge,"years old on Mars.")

我们除可以将整数转换为浮点数之外,还可以将浮点数转换为整数,不过在这个过程中,浮点数小数点之后的数字将直接被截断而不会做任何舍入:

1
2
fmt.Println(int(earthDays))
//打印出365

除整数和浮点数之外,有符号整数和无符号整数,以及各种不同长度的类型之间都需要进行类型转换。诸如 int8 转换为 int32 那样,从取值范围较小的类型转换为取值范围较大的类型总是安全的,但其他方式的类型转换则存在风险。

例如,因为一个 uint32 变量的值最大可以是 40 亿,而一个 int32 变量的值最大只能是 20 亿,所以并不是所有 uint32 值都能安全转换为 int32 值。与此类似,因为 int 类型可以包含负整数,而 uint 类型不能包含负整数,所以只有值为非负整数的 int 变量才能安全转换为 uint变量。

go语言之所以要求用户在代码中显示地进行类型转换,原因之一就是为了让我们在使用类型转换的时候三思而后行,想清楚转换可能引发的后果。

类型转换的危险之处

类型转换导致溢出会得到与目标不同的值。

在将 float64 类型转换为 int16 类型时可能会得出一个超出范围的值,从而导致软件异常。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)
func main(){
var bh float64=32768
var h=int16(bh)
fmt.Println(h)
}
//运行结果
-32768

通过 math 包提供的最小常量和最大常量,我们可以检测出将值转换为 int16 类型是否会得到无效值:

1
2
3
if bh < math.MinInt16 || bh>math.MaxInt16{
//处理超出范围的值
}

注意:因为 math 包提供的最小常量和最大常量都是无类型的,所以程序可以直接使用浮点数 bh 去跟整数 MaxInt16 做比较。

字符串转换

正如如下代码所示,我们可以像转换数字类型时那样,使用相同的类型转换语法将 rune 或者 byte 转换为 string。最终的转换结果跟我们之前在前面使用格式化变量 %c 将符文和字节显示成字符时得到的结果是一样的。

1
2
3
4
5
6
7
8
var pi rune=960
var alpha rune=940
var omega rune = 969
var bang byte = 33

fmt.Print(string(pi),string(alpha),string(omega),string(bamg))
//运行结果
πάω!

正如之前所述,因为 rune 和 byte 不过分别是 int32 和 uint8 的别名而已,所以将数字代码点转换为字符串的方法实际上适用于所有整数类型。

跟上述情况相反,为了将一串数字转换为 string,我们必须将其中的每个数字都转换为相应的代码点,这些代码点从代表字符 0 的 48 开始,到代表字符 9 的 57 结束。手工处理这种转换是非常麻烦的,好在我们可以直接使用 strconv(代表 “string conversion”,也就是 “字符串转换”)包提供的 Itoa 函数来完成这一工作,就像如下代码清单所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"strconv"
)
func main(){
countdown:=10
str:="Launch in T minus"+strconv.Itoa(countdown)+" seconds."
fmt.Println(str)
}
//运行结果
Launch in T minus 10 seconds.

注意:Itoa是 “integer to ASCII” 也就是 “将整数转换为ASCII字符”的缩写。统一码是老旧的ASCII标准的超集,这两种标准开头的128个代码点是相同的,它们包含了(上例中用到的)数字、英文字母和常见的标点符号。

将数值转换为字符串的另一种方法是使用 Sprintf 函数,它的作用与 Printf 函数基本相同,唯一的区别在于 Sprintf 函数会返回格式化之后的 string 而不是打印它:

1
2
3
4
5
  countdown:=9
str:=fmt.Sprintf("Launch in T minus %v seconds.",countdown)
fmt.Println(str)
//运行结果
Launch in T minus 9 seconds.

另外,如果我们想把字符串转换为数值,那么可以使用 strconv 包提供的 Atoi(代表 ASCII to integer,也就是将ASCII字符转换为整数)。需要注意的是,因为字符串里面可能包含无法转换为数字的奇怪文字,或者一个非常大以至于无法用整数类型表示的数字,所以 Atoi 函数有可能会返回相应的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"strconv"
"os"
)

func main(){
countdown,err:=strconv.Atoi("10")
if err != nil{
os.Exit(0)
}
fmt.Println(countdown)
}
//打印出10

如果函数返回的 err 变量的值为nil,那么说明没有发生问题。

静态类型
在go语言中,变量一旦被声明,它就有了类型并且无法改变它的类型。这种机制被称为静态类型,它能够简化编译器的优化工作,从而使程序的运行速度变得更快。

转换布尔值

Print系列的函数可以将布尔值ture和false打印成相应的文本。如下代码就展示了如何使用 Sprintf 函数将布尔变 launch 转换为文本。如果想把布尔值转换为数字值或者其他文本,那么一个简单的if语句就能满足你的要求了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
)

func main(){
launch:=false
launchText:=fmt.Sprintf("%v",launch)
fmt.Println("Ready for launch:",launchText)
var yesNo string
if launch{
yesNo="yes"
}else{
yesNo="no"
}
fmt.Println("Ready for launch:",yesNo)
}
//运行结果
Ready for launch: false
Ready for launch: no

因为go允许我们直接将条件比较的结果赋值给变量,所以跟上述转换相比,将字符串转换为布尔值的代码会更为简单。

1
2
3
4
5
yesNo :="no"
launch := (yesNo == "yes")
fmt.Println("Ready for launch:",launch)
//运行结果
Ready for launch: false

没有提供专门的布尔类型的编程语言通常会使用数字 0 和空字符串 “” 来表示 false,并使用数字 1 和非空字符串来表示 true。但是在go语言中,布尔值并没有与之相等的数字值或者字符串值,因此尝试使用 string(false)、int(false) 这一的方法来转换布尔值,或者尝试使用bool(1)、bool(“yes”)等方法来获取布尔值,go编译器都会报告错误

后言

参考书籍:Go语言趣学指南
参考课程:Go语言编程快速入门(Golang)