Go 语言实战(3): 类型

这期讲 Go 的类型。

Go 的数据类型分为基本类型和派生类型。篇幅关系,这期主要讲 基本类型,派生类型简单带过。

往期内容:

类型

下面的内容,会反复提到一个词:零值。如果声明一个变量,却不指定它的值,又或者直接 new(T) (T 是某个类型)申请一块内存,Go 会把这块内存置零。但同样是 0,在不同的类型下,会有不同的语义。了解零值,就是要知道不同类型的默认值的含义和行为。

1. 基本类型

基本类型又分为 布尔类型、数字类型 和 字符串类型。

1.1 布尔类型

类型标识符 bool,零值为 false。bool 没有直接的字面量,truefalse 在 Go 是预定义的 bool 常量,不过使用上跟字面量没有太大区别。而很多时候,用到的不是这两个常量,而是 关系运算的结果 (关系运算符 ==!=>>=<<=)和 函数返回值。

要注意的是,不像有些语言 bool 其实是数值类型的一种特例,可以或显式或隐式转换成数值。Go 的 bool 不是数值,也无法转换为数值,无法参与任何 数值运算(加减乘除) 和 位运算(按位与、或、取反等);反之,数值也不能转换为 bool

假设现在有 int 数组 nums ,要统计其中大于 0 的数的个数:

C 里面可以这样

1
2
3
4
5
6
7
8
int size = sizeof(nums) / sizeof(int);
int count = 0;
for (int i = 0; i < size; i++) {
// C 里面没有专门的布尔值,所以 true 本来就以整型数 1 代表
// 反过来,所有非零值都会被看作 true
// 括号非必要,只是为了看起来清晰
count += (nums[i] > 0);
}

但在 Go 里会报错

1
2
3
4
5
6
var count int = 0
for i := 0; i < len(nums); i++ {
// 编译器报错 cannot convert nums[i] > 0 (untyped bool value) to int
// 即使加上显式转换 int(nums[i] > 0) 也不行
count += (nums[i] > 0)
}

只能老老实实用 if

1
2
3
4
5
6
var count int = 0
for i := 0; i < len(nums); i++ {
if nums[i] > 0 {
count++
}
}

1.2 数字类型

包括整型数、浮点数、复数等,区分有符号、无符号,还有不同字节长度选择(对应不同的内存占用和数值范围),有多种组合,具体看表格。

1 字节 2 字节 4 字节 8 字节 16字节 架构相关
无符号整型 uint8 uint16 uint32 uint64 uint
有符号整型 int8 int16 int32 int64 int
浮点数 float32 float64
复数 complex64 complex128
特殊类型 byte rune uintptr
  • 数字类型的零值均为 0,各种意义上的零(整数零 0 ,浮点数零 0.0 , 复数零 0+0i)。
  • 数字类型的字面量,涉及类型推断,参考上一期的字面量部分。
  • byteuint8 的别名,runeint32 的别名。类型别名以后再详细展开,你只要知道它们是同一个类型就可以了。
  • 除了复数以外,所有数值类型的变量之间可以互相转换。 规则参考上期的类型转换部分。
  • 复数需要经由内置函数提取 实部 和 虚部(都是浮点数),或者将两个浮点数组合成复数。如果不是科学计算,一般很少用到复数,可以留个印象,用到再查。
位宽、范围与精度

位宽是类型后面的数字,它表示该类型占用了多少个二进制位。因为计算机以 字节(byte,等于 8 bit)为组织单位,位宽总是 8 的 2 整数次方倍。

uintintuintptr 三个类型的位宽与系统架构相关。其中 uintint 不小于 32 位,在 64 位系统则为 64 位;但需要注意它们是独立的类型,以 int 在 64 位系统为例,尽管范围完全一样,int 变量跟 int64 变量之间仍需转换。

uintptr 的范围则保证可以存下当前系统架构下的地址,无需特别考虑。

既然有不同的字节长度,就涉及到表示范围。范围太小,会导致数字溢出;范围太大,则引起内存的浪费。当然,如果只是个别变量,几个字节的差异可以忽略,可以直接用范围较大的类型;但涉及大数组或者大量生成的结构体的字段,则最好确定数据范围然后挑选合适的类型。(事实上,如果真的有需要节省空间和访问时间,还得考虑 内存对齐(memory alignment)或者 数据结构对齐(data structure alignment),那又是另一个话题了。)

  • 对于无符号整型,假设位宽为 N(下同),表示范围为 $ 0 \sim 2^N - 1 $ ,如 uint8 表示的最大值为 $ 2^8 - 1 = 255 $ 。这个值除了自己算,也可以通过 math 包的常量 math.MaxUint8 获得。16、32、64 位以此类推。
  • 对于有符号整型,最高位表示符号位,正负数的范围分别只有无符号数的一半(实际上因为不需要表示 负零,负数范围多一个),表示范围为 $ -2^{N-1} \sim 2^{N-1} - 1 $ ,如 int8 最小值为 -128,最大值为 127。这两个值也可以通过 math 包的常量 math.MinInt8math.MaxInt8 获得。其它有符号整型以此类推。
  • 浮点数实际上是 科学记数法 的二进制实现,二进制位除了最高位的符号位,剩下的分成了 尾数 和 指数 两部分。跟整型不同的是,浮点数的范围包括 绝对值最大值 和 非零绝对值最小值(跟指数位宽有关),还有小数精度的问题(跟尾数位宽有关)。这些规范其实是 IEEE-754 国际标准规定的。因为精度还涉及进制转换问题,就不在这里展开。float32 的绝对值范围可以通过 math.SmallestNonzeroFloat32math.MaxFloat32 获得。64 位以此类推。
  • 复数实际上由实部和虚部两个浮点数(各占一半位宽)组成,具体范围参考浮点数。

既然数字类型都有表示范围,而浮点数还可能有精度损失(有可能是超出尾数范围,也可能是进制转换造成),那么就有可能表示范围或者精度不足。如果需要表示超出范围的值,或者涉及金钱等业务需要非常高的精度,则需要用到 math/big 包的几个大数类型,包括整型 big.Int 、浮点型 big.Float 和 分数 big.Rat 。留个印象,后续再介绍。

1.3 字符串类型

类型标识符 string ,就是一串有固定长度的字符序列。

  • 字符串的底层是一个 byte 数组(以 UTF-8 编码,所以支持中文),可以当做 byte 数组访问读取。例如读取 str[i] 得到的就是一个 byte 类型的值。
  • 字符串是不可变类型,内容不可修改;对字符串变量的修改实质上是整个替换。试图修改字符串的内容(如 str[0] = 'a'),会引起报错。而 str1 = str1 + str2 中则是合法,因为 str1str2 的内存都没有被修改,而是开辟新的内存存放拼接后的字符串,然后 str1 指向新的内存。原来 str1 的内存如果没有被其它地方引用,会在后续的 GC 被回收。这点有点像 Java 的 String。大量频繁拼接字符串的场景,需要考虑优化。
  • 如果需要修改,则要转换成切片 []byte 或者 []rune ;修改完之后也可以重新转换为 string无论哪个方向的转换,内存都发生了拷贝(copy),返回的新切片 / 新字符串指向新分配的内存,所以互不影响。 频繁转换时需要考虑性能损耗。
  • []byte[]rune 差别是前者每个字符的范围是只有一个字节的 Unicode (只存得下 ASCII 码 + 拉丁符号扩展1),后者则是 四个字节的 Unicode。包含中文等等内容、字符编码可能大于一个字节的字符串只能转换为 []rune 否则会出现乱码;反之 ASCII 码的内容转换为 []rune 并不影响内容正确性,只是有一定的性能浪费而已。
  • 跟很多语言不同,Go 字符串的零值是空串 "" 而不是特殊的空值 nilnullNone 等。未初始化的字符串和空白字符串不像 Java 那样需要区分,判断都是 if str == ""
  • 字符串有两种字面量形式,除了以双引号包裹内容,还可以用反引号(` ,Esc 下方的键)包裹。区别是双引号内支持转义,不支持换行;反引号字符串恰恰相反,不支持转义,所有字符都会原样保留,包括换行(但为了跨平台兼容性,换行符统一替换成 newline,去掉 carriage return)。反引号字符串常用于一大段的内容(为了保留换行)和 正则表达式(为了保留特殊符号不被转义)。
  • 字符串支持的转义字符跟字符字面量一致,只有一个差别:不支持 \' 改为支持 \" ;这是因为字符串以双引号界定,单引号不再是特殊字符。

string 本身值得专门开一篇文章,先说这么多。

2. 派生类型

派生类型,又叫衍生类型。顾名思义,它是在其它类型的基础上衍生出来的。

一个派生类型,严格来说,是一个大类,底下可以包含多种具体类型。例如 *int*bool 虽然同为指针类型,但由于指向的类型不同,它们也是不同的类型(称作 int 指针 和 bool 指针);int 数组 和 bool 数组也是不同的类型;甚至,长度为 10 的 int 数组 [10]int 和 长度为 11 的数组 [11]int 也是不同的类型。

派生类型是个比较大的话题,个别类型光一个类型就够写一篇文章,所以这里不详细展开,只作简单罗列:

  • 指针:从 C / C++ 一脉相承,内存管理的高阶操作;不过 Go 的指针比 C / C++ 要简单和安全得多,类型安全,也不用关心内存的释放和悬挂指针。

    var iptr *int 声明了一个指向 int 的指针 iptr ,零值为 nil (相当于某些语言的 nullNone )。

  • 数组(array)和 切片(slice):类似其它语言的数组和动态数组。(注意这是两个不同类型,只是性质相近一起介绍)

    var a [10]int 声明了一个长度为 10 的 int 数组 a,零值为成员都是零值的数组,可以直接使用。

    var s []int 则声明了一个 int 切片 s,零值为 nil

  • 映射(map):类似其它语言的 map。不过不像 C++ 和 Java 作为库引入,Go 的 map 是语言内置的。因为 Go 没有 集合(set),很多时候也需要用 map 模拟。

    var m map[string]int 声明了一个 key 为 string ,value 为 int 的 map m ,零值为 nil

  • 函数(function):Go 里面函数也是第一等公民。函数既然是一种类型,那么就可以作为变量和参数。Go 支持函数式编程。

    var f func(int)bool 声明了一个 『接受一个 int 参数并返回一个 bool 值的函数』变量 f ,零值为 nil

    当然你也可以用 func 关键字直接声明一个函数 f

    1
    2
    3
    func f(a int) bool {
    // 函数体
    }

    两者都是通过 f() 调用(当然,实际调用要提供一个 int 参数)。后者只能用于全局(包级)函数,必须给出函数体,f 不是一个变量,f 的值是一个具体的函数而且不能修改。从效果上接近一个函数常量(但不是)。

  • 结构体(struct):类似 C / C++ 的结构体,但是可以定义行为(方法)。Go 没有 类(class)和 继承(inheritance),而是通过 结构体 和 组合(composition)实现面向对象。

    下面声明了一个『拥有一个 string 字段 和一个 int 字段 的结构体』的变量 alice ,零值是所有字段都为对应零值的结构体:

    1
    2
    3
    4
    var alice struct {
    name string
    age int
    }

    但这样写,每次声明都要把结构体重新写一遍,啰嗦还容易错——只要字段的名称、类型或者顺序,随便一样有差别,都会被认为是不同的类型。一般情况下,除非这个结构体只使用这么一次,否则都不应该这样写。

    正确的做法,是给结构体一个类型名,然后用名字来声明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type person struct {
    name string
    age int
    }
    var (
    alice person
    bob person
    )

    在这里 person 成了一个新的自定义类型,与之相对应,前面没有定义名称的结构体称为匿名结构体。

  • 接口(interface):接口是一系列行为(方法签名,方法是一种特殊的函数)的集合。跟其它语言不同,Go 的接口不需要显式声明实现(implementation),一个类型只要实现了接口的所有方法,它就隐式地满足接口。是否满足接口可以在编译期静态检查,所以是类型安全的。Go 实现了类型安全的鸭子类型(duck typing) 。这种设计是 Go 的组合式面向对象的重要组成部分。

    跟结构体类似,接口的定义比较长,也应该定义成一个自定义类型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type runner interface {
    // 满足 runner 接口的类型,应该具有 walk() 和 run() 两个方法
    walk() // walk 是一个没有参数也没有返回值的方法
    run(int)bool // run 方法接收一个 int 参数,并返回一个 bool 值
    }
    var (
    cindy runner
    danny runner
    )

    这里声明了两个 『拥有两个方法的接口』的变量。接口变量的零值是 nil ,可以接受任何满足接口的类型的值。

  • 通道(channel):channel 用于并发时在协程间通信,是 CSP 模型的重要部分。

    channel 除了区分传递的消息的类型,还分读写和缓冲区大小。其中缓冲区在初始化时决定,剩下的在类型上体现:

    1
    2
    3
    4
    5
    var (
    ich chan int // 可以读写的通道,消息类型是 int
    bch <-chan bool // 只读的通道,消息类型是 bool
    fch chan<- float64 // 只写的通道,消息类型是 float64
    )

    channel 的零值为 nil

关于派生类型,最后补充两点:

  1. 所有零值不是 nil 的变量,都可以声明之后直接使用(只不过值都是零);而零值为 nil 的类型,意味着需要额外的初始化,其中 切片(slice)、映射(map)和 通道(channel)都是通过内置函数 make() 申请内存并初始化。

  2. 先定义为自定义类型,再用新类型声明变量,对 结构体 和 接口 来说,既不是必选项,也不是特权。这句话的意思是:

    • 不是必选项:匿名结构体 和 匿名接口 也是合法的代码。只不过由于这两种类型的定义太长(一定会换行),匿名使用会有很多麻烦,还是推荐先定义类型。这虽然不是强制选项,却是最佳实践。
    • 不是特权:只要你愿意,所有类型都可以定义为自定义类型(参考下一节),只不过有没有必要而已。

这部分内容只是为了让大家对派生类型有一个整体的印象。细节会在用到这些类型时详细展开。

3. 自定义类型

Go 使用 type 关键字自定义类型,有两种用法:

3.1 类型定义

语法 type TypeName TypeDefinition

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
type NewInt int64
type NewStruct struct {
val int
}
type Bitmap []uint64
type Runner interface {
Run()
}
type Handler func(req Request)Response

类型定义 看起来很像 C / C++ ,只是把名字移到了前面:

1
2
3
4
// C
typedef struct {
int val;
} NewStruct;

或者 Java 的

1
2
3
4
// Java
class NewStruct {
int val;
}

虽然像,差别也很明显:

  • Java 里,新名字只能是一个类(即使里面只有一个值);Go 既能创建新结构体 / 接口,也能复用已有的类型——包括基本类型和内置的派生类型。
  • C 的 typedef 倒是也可以用于基本类型,但得到的实际上是一个别名,新旧类型仍然是同一种类型;而 Go 声明了一种新的类型。

type NewInt int64 举例,虽然它们共享同样的内存实现(8 个字节的连续内存),在基本运算符上有同样的结果,但是 NewInt 被认为是跟 int 不同的一个类型,可以拥有自己的方法。两种类型不能直接一起运算,也不能用作另一种类型的参数,需要经过转换。

不直接使用原类型,而是定义命名的新类型,我认为有以下几个原因:

  1. 使用方便。

    这是对冗长的派生类型——尤其是 结构体 和 接口 而言的。简洁的名字当然比冗长的结构方便且不易出错。

  2. 自注释。

    名字可以体现用途和意图。

  3. 借用静态检查发现错误。

    将底层实现一样但是业务逻辑不一致的类型分别定义为不同的类型,可以借由静态检查发现逻辑错误。这在上一期类型转换部分,我用 砧板 和 地板 的 底层类型都是 木板 做了一个类比。

  4. 添加方法。

    Go 可以(且只可以)给当前包定义的类型添加方法。内置类型和导入类型定义成新类型之后,就可以给新类型添加方法,实现面向对象编程。

    需要注意的是,定义成新类型之后,原来类型的方法就全部丢失,不能再访问了(毕竟已经不是同一个类型)。如果需要保留原来的方法,应该选择将旧类型匿名嵌入新类型的结构体。匿名嵌入效果上接近继承,实际上是组合,只是跟一般成员组合相比,被匿名嵌入类型的成员和方法可以直接访问。具体在 方法 和 结构体 部分展开。

3.2 类型别名

语法 type TypeAlias = AnotherType

例子:

1
2
type IntAlias = int
type StructAlias = NewStruct

类型别名 type IntAlias = int 中,IntAlias 被认为是 int 的别名,看作是同一类型,可以直接一起运算或者作为参数,无需转换。类型别名自 Go 1.9 引入,用来解决迁移、升级等重构场景下,类型重命名的兼容性问题,以及方便引用外部导入的类型。

实际上,类型别名仅在代码中存在,编译时会全部替换成实际的类型。只有类型定义产生了新的类型。

4. 枚举(模拟)

说到自定义类型,就顺便提一下枚举。

在数学和计算机科学上,枚举是指列出一个有限集合的所有成员。而枚举类型是一种特殊的类型,只能取限定的某几个值。有些语言只是限定了枚举类型的取值(C / C++);而有些语言则(以常量的形式)直接预先初始化了枚举类型所有可能值的实例,变量不仅仅只能取有限的值,而是只能是这几个实例之一(Java,Python)。

Go 没有提供对枚举的支持。是的,Go 跟谁都不像,根本没有枚举。相对应地,在 Go 里一般通过 自定义类型 + 常量 模拟枚举。我们来看看官方库里面 time 包对 月份 Month 和 周几 Weekday 的定义:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package time
// A Month specifies a month of the year (January = 1, ...).
type Month int
const (
// 如果理解不了这部分,请翻看上一期常量部分关于自动补全和 iota 的内容
January Month = 1 + iota
February
March
April
May
June
July
August
September
October
November
December
)
// String returns the English name of the month ("January", "February", ...).
func (m Month) String() string {
if January <= m && m <= December {
// longMonthNames 是一个预先定义的字符串切片,里面按顺序保存了每个月份的字符串
return longMonthNames[m-1]
}
// 如果 m 的值不在范围内,返回表示错误的字符串
buf := make([]byte, 20)
n := fmtInt(buf, uint64(m))
return "%!Month(" + string(buf[n:]) + ")"
}
// A Weekday specifies a day of the week (Sunday = 0, ...).
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
// String returns the English name of the day ("Sunday", "Monday", ...).
func (d Weekday) String() string {
if Sunday <= d && d <= Saturday {
// 跟月份类似,longDayNames 也是预先定义好的字符串切片
return longDayNames[d]
}
// 同样地,超出范围就返回表示错误的字符串
buf := make([]byte, 20)
n := fmtInt(buf, uint64(d))
return "%!Weekday(" + string(buf[n:]) + ")"
}

在这个例子里,MonthWeekday 都是底层类型为 int 的自定义类型;然后这两个类型定义了一系列的常量作为取值范围,并且定义了一个 String() 方法,返回对应值的字符串形式。

在 1.4 之后,Go 工具链提供了 go generate 命令,配合 stringer 工具可以自动生成常量的 String() 方法。除此之外,也可以按需给新类型添加各种方法,模拟其他语言里的枚举,或者增加需要的功能。详情可以自己查阅,这里不再展开。

有独立的类型、通过常量给定取值、能返回字符串,肯定比直接用一个整型数来表示要强。自定义类型被认为是跟原来不一样的类型,在赋值或者传参过程中,如果使用了不同类型的变量,直接在编译时就报错了;另一方面,如果只通过常量引用这些 “枚举类型” 的值,取值范围也限制住了。基本实现了枚举的目的。

但也只能说部分实现。提供的常量只是给出了范围的建议值,而不是强制值。Go 没有提供『把类型的取值范围硬性限制在某几个值』的语义。新类型的取值范围仍然是和底层类型一样:

1
2
3
4
5
6
7
8
9
import "time"
func main() {
// 这是合法的,-1 作为字面量是 untyped 的,可以自动转换类型
var m1 time.Month = -1
// 又或者是忘了赋值,那么 m2 的值就为 零值,即 0,也不在常量范围内
var m2 time.Month
}

如果说 m1 是违反了(模拟的)枚举类型只使用常量引用的原则,注意一下就可以避免;那么 m2 这种忘记赋值的情况可能更难发现一些。

既然没有办法通过类型安全本身限制取值,就只能在使用时注意判断值的范围,特别要处理意外的情况。在判断枚举值时,一般使用 if-else 或者 switch-case 代码块,这时记得加上一个 else 或者 default 处理无效值。又或者干脆给类型添加一个 IsValid()bool 方法,判断值是否有效。

当然这种实现方式也并不全是坏处。Go 没有限定枚举必须是什么样子,就可以按自己的需要设计:

  • 枚举的底层值不一定是整数,可以是任意基本类型。
  • 枚举的值允许重复;这个有些时候可能会引起错误,有些时候又可能有用。
  • 没有增加特殊情况,保持了类型系统的简单和高效;引入专门的枚举类型其实涉及很多问题,例如序列化和反序列化怎么处理。

总的来说,我个人觉得在枚举这个问题上处理得不够好,好像不太符合 Go 一向强类型和自带最佳实践的风格。当然也有可能是我理解得不够深入。但无论怎么说,目前的实现方式在官方库非常普遍,出于兼容性的考虑,至少在 Go 1.x 阶段不太会改动的样子。那就接受这个设定,并且小心避开潜在的坑吧。

练习

问题1 :

假设程序中需要储存一个状态,有 待办、进行中、完成 三种状态,应该怎么样定义类型?

如果这样的状态需要储存非常多个,定义成一个大数组储存,该如何节省空间?

问题 2:

问以下程序的输出,为什么。老规矩,不用运行就得出答案更佳。

1
2
3
4
5
6
7
func main() {
var str1 = "hello world"
var tmp1 = "hello"
var tmp2 = "world"
var str2 = tmp1 + " " + tmp2
fmt.Println(str1 == str2)
}



上期练习答案

第二期最后的练习,答案如下

如果还没做练习,不要直接看答案




















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
26
27
28
29
30
31
32
33
34
35
36
// 全局常量和变量是经过依赖分析后统一初始化的
// 只要没有依赖环,声明先后不影响引用
// 但局部常量和变量不能这样做
const (
A uint8 = B // uint8, 255
B = 255 // 整型字面量默认类型为 untyped int,255
)
var (
a uint64 = A / 2 // 类型报错!A 的 uint8 是显式类型,2 自动转换为 uint8 参与运算,所以 A / 2 表达式整体也是 uint8,不能自动转换为 uint64
b int8 = B / 2 // B 是 untyped 无类型,表达式整体也是 untyped int,结果自动转换为 int8,结果为 127(整数除法),刚好不超过 int8 的范围
)
func main() {
// 以下的 A, B, a, b 均没有重复声明,这是因为作用域不同,发生了 shadow
A := byte('0') // '0' 为 untyped rune,显式转换为 byte,值范围没有超出,所以类型为 byte;byte 实际是 uint8 的别名,'0' 的值为 48,只有作为字符格式化时才输出字符 '0'
B := byte('1') // 类型同上,值为 49
C := A - B // A, B 均为 byte 类型,所以表达式的结果也是 byte,C 也是 byte;计算结果为 -1,但 byte(即 uint8,无符号 8 位整型)最小值是 0,下溢出,截取得到 255 (惊不惊喜,意不意外);这种截取很容易导致意料以外的结果,需要尽量避免
fmt.Println(C) // 输出 255
const a = '0' // 没有指定类型,类型为 untyped rune,值为 48
const b = '1' // 类型为 untyped rune,值为 49
fmt.Println(byte(a - b)) // 类型转换错误!a - b 的结果类型为 untyped rune,值为 -1,转换成 byte 会下溢出;常量在转换中不允许溢出
i := 0x1e+2 // 这其实是一个表达式,格式化后会自动在加号两边添加空格;加号前面的数是十六进制的 30,整体就是 30 + 2;两个数都是整型字面量,所以表达式和变量的类型都是 untyped int,值为 32
j := 1e+2 // 浮点数字面量,表示 1 x 10^2,即 100,类型为 untyped float;注意 j 没有任何有效使用,会报 not used 错误!
for i = 0; i < 1; i++ { // i 在这里是赋值,在循环条件 i < 1 中算有效使用(涉及到了循环,略有超纲,但有其它语言经验的话问题不大)
for j := 0; j < 1; j++ { // 短声明产生了一个新的 j ,跟前面的 j 无关
// 空循环体
}
}
// 现在 i, j 分别是多少
// i 在循环中先是被赋值为 0,然后自增,到 1 时不满足循环条件退出,所以 i 为 1
// (外层)j 没有被使用,还是 1e+2 (100)
}

你答对了吗?


知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议
本文为本人原创,采用知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议进行许可。
本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。
请点击查看协议的中文摘要。