Skip to content

2 变量与数

变量定义

前面提到 Go 是静态类型语言:变量的类型必须显式定义或者赋初值以自动推断;类型与变量绑定,对于已定义的变量,其类型不能变动。

变量有几种定义方式:

go
var foo int      // 无初值,指定类型
var foo int = 3  // 有初值,指定类型
var foo = 3      // 有初值,类型自动推断
foo := 3         // 有初值,类型自动推断

这里 var 是定义变量用的关键字,foo 是变量名,int 是类型。如果直接赋初值,也可以略去类型不写,直接自动推断。整数会默认推断为 int

:= 是 Go 提供的定义变量用的缩写符号。算是其简洁性的一个代表了。

在 JS 中,如果定义变量却不赋初值,则初值为 undefined。Go 的做法则是:如果你不写初值,它帮你赋了。不同类型会有自己的默认初值,例如,int 型的默认初值为 0

如果要定义常量,则把 var 换成 const。常量必须有初值。

go
const bar int = 8
const bar = 8

至于变量的作用域、:= 的限制等等,我们稍后对 Go 更熟悉一些之后再讨论。

整型

刚才提到了整型 int,Go 中有以下几种整型:

  • 有符号整型:intint8int16int32int64
  • 无符号整型:uintuint8uint16uint32uint64

intuint 的长度取决于当前平台。对于带数字的整型,那个数字就是整型所占的二进制位数,例如 int16 就是 16 个二进制位,取值范围为 [215,2151] 即 -32768 ~ +32767。

也就是说相比于 C / C++ 中 shortlonglong long 这些长度全部由平台决定,Go 中只有 intuint 的长度由平台决定,其余整型均指明长度。

整型的默认初值均为 0

整数的存储与运算这里和 C / C++ 是一致的,负数以补码形式存储,运行时溢出不视为错误。只有在写字面量赋值的时候如果溢出,编译器会报错。

go
var foo int16 = 32768 // 编译期报溢出错误 (overflow)

var bar int16 = 32767
bar++ // 运行时不报错
fmt.Println(bar) // -32768

这里附上一份各整型取值范围表。基本上见多了之后都记住了。

长度有符号范围无符号范围
8 位min=27=128
max=271=+127
min=0
max=281=255
16 位min=215=32768
max=2151=+32767
min=0
max=2161=65535
32 位min=231=2147483648
max=2311=+2147483647
min=0
max=2321=4294967295
64 位min=2631018
max=2631+1018
min=0
max=26411019

浮点

Go 中提供 IEEE 754 的单精度和双精度浮点:float32float64。注意没有不带数字的 float。两种浮点的默认初值均为 0

其中 float64 就是 JS 中日常使用的数字类型,8 字节浮点中 1 位符号位、11 位阶码、52 位尾数。float32 则是 4 字节,1 位符号位、8 位阶码和 23 位尾数。

所以换算到十进制,它们的精度分别是

  • float32223+11=7,十进制下保证 7 位有效数字可靠
  • float64252+11=15,十进制下保证 15 位有效数字可靠

JS 中有浮点导致的最大安全整数 ±2531,在这里就不需要了 —— 我们有真正的整型了,没必要用浮点存整数了。

关于浮点数长度、精度、数值安全性这一部分的详细推导,可以看我此前的笔记:JS 中的数值存储与安全

Go 中浮点运算法则与 IEEE 754 中的一致。非零数除以零会产生 +Inf-Inf,零除以零会产生 NaN。但是要注意,Go 中没有 InfNaN 字面量,需要通过 math 包获取:

go
import "math"

math.NaN()   // NaN
math.Inf(1)  // +Inf
math.Inf(-1) // -Inf

需要注意的点

整型除法

不同于 JS 只有浮点的世界,整型的除法与浮点有不同:

  • 整型除法的结果是整数,结果为向零截断的商;
  • 取模使用 % 符号,结果也是整数;
  • 除以零会产生运行时错误!而不是产生 InfNaN 而不报错。

除此之外,Go 没有在浮点数上定义模运算!例如下面的操作会产生错误:

go
foo := 3.0
bar := 2.0
result := foo % bar // Err: operator % not defined

不允许跨类型运算

Go 不允许跨类型运算,这是其类型安全性的一个体现。例如下面的操作会产生错误:

go
floatNum := 10.0
intNum := 5
result := floatNum / intNum // Err: mismatched types

即使都是整型,也不允许直接运算:

go
var foo int32 = 1
var bar int64 = 2
result := foo + bar // Err: mismatched types

正确的做法是执行类型转换,语法是和 C 系类似的 Type(value)

go
floatNum := 10.0
intNum := 5
result := floatNum / float64(intNum)

禁止跨类型运算能让数值语义保持明确,能解决很多问题,例如

  • 不会误把数字和字符串 + 在一起
  • 避免不同精度的数混用导致精度丢失
  • 不会因为隐式类型转换产生难以发现的逻辑问题

+0-0

如果在 Go 中尝试定义 -0 字面量:

go
negZero := -0.0

编译器一般会提示 -0.0 字面量与 0.0 等价:in Go, the floating-point literal '-0.0' is the same as '0.0', it does not produce a negative zero。

但既然遵循 IEEE 754,那就必然会有 +0-0 两兄弟。尽管不能字面定义,在运算过程中仍然会产生:

go
zero := 0.0

negZero := -zero            // -0
negZero2 := -1 / (1 / zero) // -0

同样地,根据 IEEE 754,+0-0 是相等的:

go
zero := 0.0

fmt.Println(zero == -zero) // true

如果需要「就地」产生 -0,需要使用 math 包:

go
negZero := math.Copysign(0, -1.0)

划重点

  • 类型举足轻重
    • 未赋初值的会按类型自动赋初值
    • 变量的类型始终不变
    • 类型决定了能完成什么运算
    • 不同类型值严格区分,不允许混合运算
  • 整型和浮点的心智模型不同于 JS 只有浮点
    • 需要多大的整数(8、16、32、64,有无符号)可以自己定义
    • 除以零会产生错误
    • 浮点数不能模(%