Go语言中10种不同的整数类型以及使用方法

异步社区官方博客

本章学习目标

Go提供了10种类型用于表示整数,它们被统称为整数类型(integer)。整数类型不能存储分数,也不会出现浮点类型的精度问题,但因为每种整数类型的取值范围都各不相同,所以我们应该根据场景所需的取值范围来决定使用何种整数类型。

请考虑这一点

你可以用两个记号(token)表示多少个数字?

如果这两个记号可以按位置进行区分,那么它们将有4种可能的排列方式:两个标识都存在;两个标识都不存在;只有一个标识存在;只有另一个标识存在。这4种排列方式的每一种可以表示一个数字,因此两个记号最多可以表示4个数字。

与此类似,计算机使用二进制位表示数字,每个二进制位的值要么为1,要么为0,这两个值分别表示打开和关闭两种状态。基于上述排列原理,使用8个二进制位总共可以表示256个不同的值。按照这种方法计算,我们需要使用多少个二进制位才可以表示数字4 000 000 000?

7.1 声明整数类型变量

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

var year int = 2018

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

var month uint = 2

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

year := 2018
var year = 2018
var year int = 2018

提示 正如第6章的浮点类型例子所示,如果类型推断可以正确地为变量设置类型,那么我们就没有必要为其指定int类型。

速查7-1

如果你的水杯里面有半杯水,你会选择哪种整数类型来表示水杯中的水有多少毫升呢?

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

无论是有符号整数还是无符号整数,它们都有各种不同大小(size)的类型可供选择,而不同大小又会影响它们自身的取值范围以及内存占用。表7-1列出了8种与计算机架构无关的整数类型,以及这些类型需要占用的内存大小。

{40%}

表7-1 与计算机架构无关的整数类型

类型 取值范围 内存占用情况
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字节)

正如表7-1所示,Go提供了非常多的整数类型可供选择。本章稍后将会介绍其中一些类型的应用场景,并说明当程序超出类型的有效取值范围时会发生什么事情。

因为int类型和uint类型会根据目标硬件选择最合适的位长,所以它们未被包含在表7-1里面。举个例子,在诸如Go Playground、Raspberry Pi 2和旧款手机等32位架构上,intuint都是32位值,而较新的计算机都基于64位架构,所以这些架构上的intuint都是64位值。

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

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

速查7-2

哪种整数类型的值可以是–20 151 021?

7.1.2 了解类型

正如代码清单7-1所示,如果你对Go编译器推断的类型感到好奇,那么可以使用Printf函数提供的格式化变量%T去查看指定变量的类型。

代码清单7-1 检视变量的类型:inspect.go

year := 2018
fmt.Printf("Type %T for %v\n", year, year)  ←--- 打印出“Type int for 2018”

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

days := 365.2425
fmt.Printf("Type %T for %[1]v\n", days)  ←--- 打印出“Type float64 for 365.2425”

速查7-3

被双引号包围的文本、整数、实数以及(没有被双引号包围的)单词true,你知道Go语言会为它们推断什么类型吗?请扩展代码清单7-1,声明多个变量并为它们分别赋予上述提到的各个值,然后执行程序,看看Go语言会为它们推断何种类型。

7.2 为8位颜色使用uint8类型

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

var red, green, blue uint8 = 0, 141, 213

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


Go语言中的十六进制数字

层叠样式表(CSS)通过十六进制数字而不是十进制数字来指定颜色。与十进制只使用10个数字相比,十六进制需要多用6个数字:其中前10个数字跟十进制一样,都是0~9,但是之后的6个数字是十六进制数字A ~ F。十六进制中的A相当于十进制中的10,B相当于11,以此类推,直到相当于15的F为止。

十进制对拥有十根手指的人类来说是一种非常棒的数字系统,但与之相比,十六进制更适合计算机。这是因为一个十六进制数字需要消耗4个二进制位,也就是半字节(nibble),而2个十六进制数字则正好需要消耗8个二进制位,也就是1字节,这也使十六进制可以非常方便地为uint8设置值。

下表展示了一些十六进制数字以及与之对应的十进制数字。

十六进制数字和十进制数字

十六进制数字 十进制数字
A 10
F 15
10 16
FF 255

为了区分十进制数字和十六进制数字,Go语言要求十六进制数字必须带有0x前缀。作为例子,以下两行代码分别用十进制数字和十六进制数字定义了完全相同的3个变量:

var red, green, blue uint8 = 0, 141, 213
var red, green, blue uint8 = 0x00, 0x8d, 0xd5

在使用Printf函数打印十六进制数字的时候,你可以使用%x或者%X作为格式化变量:

fmt.Printf("%x %x %x", red, green, blue)  ←--- 打印出“0 8d d5”

为了输出能够完美适配层叠样式表文件的颜色的数字,我们需要用到格式化变量%02x。它跟之前介绍过的格式化变量%v%f一样,通过数字2指定了格式化输出的最小数字数量,并通过数字0启用了格式化的零填充功能:

fmt.Printf("color: #%02x%02x%02x;", red, green, blue)  ←--- 打印出“color: #008dd5;”

速查7-4

存储一个 uint8类型的值需要用多少字节?

7.3 整数回绕

整数类型虽然不会像浮点类型那样因为舍入错误而导致不精确,但整数类型也有它们自己的问题,那就是有限的取值范围。在Go语言中,当超过整数类型的取值范围时,就会出现整数回绕(wrap around)现象。

例如,8位无符号整数uint8类型的取值范围为0~255,而针对该类型的增量操作在结果超过255时将回绕至0。作为例子,代码清单7-2就通过执行增量操作触发了有符号和无符号8位整数的回绕现象。

代码清单7-2 整数回绕:integers-wrap.go

var red uint8 = 255
red++
fmt.Println(red)   ←--- 打印出“0”

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

7.3.1 聚焦二进制位

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

代码清单7-3 打印二进制位:bits.go

var green uint8 = 3
fmt.Printf("%08b\n", green)  ←--- 打印出“00000011”
green++
fmt.Printf("%08b\n", green)  ←--- 打印出“00000100”

速查7-5

使用Go Playground试验整数回绕。

1.代码清单7-2使用1作为rednumber的增量,如果在执行增量运算时对这两个变量加入一个更大的数字,结果会怎么样?

2.如果在red为0或者number等于-128时对它们执行减量运算,结果又会怎么样?

3.回绕不仅会在8位整数类型中出现,还会在16位、32位以及64位整数类型中出现。请声明一个uint16类型的变量,并将该类型的最大值65535赋予该变量,然后将这个变量的值加1,看看结果会怎么样?

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

在代码清单7-3中,对green的值执行加1操作将导致1进位,而0则被留在原位,最终计算得出二进制数00000100,也就是十进制数4,这个过程如图7-1所示。

0701{18%}

图7-1 在二进制加法中对1实施进位

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

代码清单7-4 二进制位在整数回绕时的状态:bits-wrap.go

var blue uint8 = 255
fmt.Printf("%08b\n", blue)  ←--- 打印出“11111111”
blue++
fmt.Printf("%08b\n", blue)  ←--- 打印出“00000000”

0702{18%}

图7-2 “无处容身”的进位

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

速查7-6

哪个格式化变量可以让你以二进制形式查看整数类型变量的值?

7.3.2 避免时间回绕

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

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

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

代码清单7-5 使用64位整数存储日期:time.go

package main

import (
    "fmt"
    "time"
)
func main() {
    future := time.Unix(12622780800, 0)
    fmt.Println(future)  ←--- 在Go Playground打印出“2370-01-01 00:00:00 +0000 UTC”
}

速查7-7

使用哪种整数类型可以避免回绕?

7.4 小结

为了检验你是否已经掌握了上述知识,请尝试完成以下实验。

实验:piggy.go

请编写一个新的储钱罐程序,让它能够用整数记录存入的美分数量而不是美元数量,然后随机地将5美分、10美分和25美分的硬币投入空白的储钱罐里面,直到存款超过20美元为止,并且以美元格式(如$1.05)打印出储钱罐在每次收到存款之后的当前余额。

提示 如果你想要计算出两个数相除的余数,那么可以使用取模操作符%


速查7-1答案

因为水杯中的水量不可能为负,所以你可以使用只能表示非负整数的无符号整数类型uint来表示水杯中的水量。


速查7-2答案

intint32int64类型的值都可以是–20 151 021。


速查7-3答案

a := "text"
fmt.Printf("Type %T for %[1]v\n", a)  ←--- 打印出“Type string for text”
 
b := 42
fmt.Printf("Type %T for %[1]v\n", b)  ←--- 打印出“Type int for 42”
 
c := 3.14
fmt.Printf("Type %T for %[1]v\n", c)  ←--- 打印出“Type float64 for 3.14”
 
d := true
>fmt.Printf("Type %T for %[1]v\n", d)  ←--- 打印出“Type bool for true”

速查7-4答案

存储一个(无符号)8位整数只需要占用1字节内存空间。


速查7-5答案

1 // add a number larger than one
  var red uint8 = 255
  red += 2
  fmt.Println(red)    ←--- 打印出“1”
 
  var number int8 = 127
  number += 3
  fmt.Println(number)   ←--- 打印出“-126”
2 // wrap the other way
  red = 0
  red--
  fmt.Println(red)   ←--- 打印出“255”
 
  number = -128
  number--
  fmt.Println(number)   ←--- 打印出“127”
3 // wrapping with a 16-bit unsigned integer
  var green uint16 = 65535
  green++
  fmt.Println(green)   ←--- 打印出“0”

速查7-6答案

格式化变量%b可以以二进制形式输出整数的值。


速查7-7答案

为了避免回绕,必须使用一种足够长的整数类型,使它能够容纳你想要存储的值。


本文摘自《Go语言趣学指南》

Go适合各种技术水平的程序员,这对任何大型项目来说都是至关重要的。作为一种相对较为小型的语言,Go的语法极少,需要掌握的概念也不多,因此它非常适合用作初学者的入门语言。

遗憾的是,很多学习Go语言的资源都假设读者拥有C语言的工作经验,而本书的目的则在于弥补这一缺陷,为脚本使用者、业余爱好者和初学者提供一条学习Go语言的康庄大道。为了让起步的过程变得更容易一些,本书的所有代码清单和练习都可以在Go Playground里面执行,你在阅读本书的时候甚至不需要安装任何东西。

如果你曾经使用过诸如JavaScript、Lua、PHP、Perl、Python或者Ruby这样的脚本语言,那么你已经做好了学习Go的万全准备。如果你曾经使用过Scratch或者Excel的公式,或者编写过HTML,那么你毫无疑问可以像Audrey Lim在她的演讲“A Beginner’s Mind”(初学者之心)中所说的一样,选择Go作为你的第一门“真正”的编程语言。虽然掌握Go语言并不是一件容易的事情,需要相应的耐心和努力,但我们希望本书在这个过程中能够助你一臂之力。