常量和变量

这篇文章将介绍常量和变量相关的知识。 类型不确定值、类型推断和值的显式类型转换等概念也将被介绍。

上一章中提到的各种基本类型的字面表示形式 (除了falsetrue)都属于无名常量(unnamed constant),或者叫字面常量(literal constant)。 falsetrue是预声明的两个有名常量。 本文将介绍如何声明自定义的有名常量。

类型不确定值(untyped value)和类型确定值(typed value)

在Go中,有些值的类型是不确定的。换句话说,有些值的类型有很多可能性。 这些值称为类型不确定值。对于大多数类型不确定值来说,它们各自都有一个默认类型, 除了预声明的nilnil是没有默认类型的。 我们在后续的文章中将了解到很多关于nil的知识。

与类型不确定值相对应的概念称为类型确定值。

上一章提到的字面常量(无名常量)都属于类型不确定值。 事实上,Go中大多数的类型不确定值都属于字面常量和本文即将介绍的有名常量。 少数类型不确定值包括刚提到的nil和以后会逐步解触到的某些操作的布尔返回值。

一个字面常量的默认类型取决于它的字面表示形式:

类型不确定常量的显式类型转换

和很多语言一样,Go也支持类型转换。 一个显式类型转换的形式为T(v),其表示将一个值v转换为类型T。 编译器将T(v)的转换结果视为一个类型为T的类型确定值。 当然,对于一个特定的类型TT(v)并非对任意的值v都合法。

下面介绍的规则同时适用于上一章介绍的字面常量和即将介绍的类型不确定有名常量。

对于一个类型不确定常量值v,有两种情形显式转换T(v)是合法的:
  1. v可以表示为T类型的一个值。 转换结果为一个类型为T的类型确定常量值。
  2. v的默认类型是一个整数类型(int或者rune) 并且T是一个字符串类型。 转换T(v)v看作是一个Unicode码点。 转换结果为一个类型为T的字符串常量。 此字符串常量只包含一个Unicode码点,并且可以看作是此Unicode码点的UTF-8表示形式。 对于不在合法的Unicode码点取值范围内的整数v, 转换结果等同于字符串字面值"\uFFFD"(亦即"\xef\xbf\xbd")。 0xFFFD是Unicode标准中的(非法码点的)替换字符值。 (但是请注意,这样的转换可能从今后的某个Go版本开始将不再允许。)

事实上,第二种情形并不要求v必须是一个常量。 如果v是一个常量,则转换结果也是一个常量。 如果v不是一个常量,则转换结果也不是一个常量。

一些合法的转换例子:
// 结果为complex128类型的1.0+0.0i。虚部被舍入了。
complex128(1 + -1e-1000i)
// 结果为float32类型的0.5。这里也舍入了。
float32(0.49999999)
// 只要目标类型不是整数类型,舍入都是允许的。
float32(17000000000000000)
float32(123)
uint(1.0)
int8(-123)
int16(6+0i)
complex128(789)

string(65)          // "A"
string('A')         // "A"
string('\u68ee')    // "森"
string(-1)          // "\uFFFD"
string(0xFFFD)      // "\uFFFD"
string(0x2FFFFFFFF) // "\uFFFD"

下面是一些非法的转换:
int(1.23)     // 1.23不能被表示为int类型值。
uint8(-1)     // -1不能被表示为uint8类型值。
float64(1+2i) // 1+2i不能被表示为float64类型值。

// -1e+1000不能被表示为float64类型值。不允许溢出。
float64(-1e1000)
// 0x10000000000000000做为int值将溢出。
int(0x10000000000000000)

// 字面值65.0的默认类型是float64(不是一个整数类型)。
string(65.0)
// 66+0i的默认类型是complex128(不是一个整数类型)。
string(66+0i)

从上面的例子可以看出,一个类型不确定数字值所表示的值可能溢出它的默认类型的表示范围。 比如上例中的-1e10000x10000000000000000。 一个溢出了它的默认类型的表示范围的类型不确定数字值是不能被转换到它的默认类型的(将编译报错)。

注意,有时一个显式转换形式必须被写成(T)(v)以免发生歧义。 这种情况多发生在T不为一个标识符的时候。

我们以后将在其它章节学到更多的显式类型转换规则。

类型推断介绍

Go支持类型推断(type deduction or type inference)。 类型推断是指在某些场合下,程序员可以在代码中使用一些类型不确定值, 编译器会自动推断出这些类型不确定值在特定情景下应被视为某些特定类型的值。

在Go代码中,如果某处需要一个特定类型的值并且一个类型不确定值可以表示为此特定类型的值, 则此类型不确定值可以使用在此处。Go编译器将此类型不确定值视为此特定类型的类型确定值。 这种情形常常出现在运算符运算、函数调用和赋值语句中。

有些场景对某些类型不确定值并没有特定的类型要求。 在这种情况下,Go编译器将这些类型不确定值视为类型为它们各自的默认类型的类型确定值。

上述两条类型推断规则可以被视为是隐式转换规则。 注意,Go白皮书中并没有隐式转换这一术语,此术语只在本书中使用。

本文下面的章节将展示一些类型推断的例子。 后续其它文章将会展示更多类型推断的例子和规则。

(有名)常量声明(constant declaration)

和无名字面常量一样,有名常量也必须都是布尔、数字或者字符串值。 在Go中,关键字const用来声明有名常量。 下面是一些常量声明的例子。
package main

// 声明了两个单独的有名常量。(是的,
// 非ASCII字符可以用做标识符。)
const π = 3.1416
const Pi = π // 等价于:Pi == 3.1416

// 声明了一组有名常量。
const (
	No         = !Yes
	Yes        = true
	MaxDegrees = 360
	Unit       = "弧度"
)

func main() {
	// 声明了三个局部有名常量。
	const DoublePi, HalfPi, Unit2 = π * 2, π * 0.5, "度"
}

Go白皮书把上面每行含有一个等号=的语句称为一个常量描述(constant specification)。 每个const关键字对应一个常量声明。一个常量声明中可以有若干个常量描述。 上面的例子中含有4个常量声明。除了第3个,其它的常量声明中都各自只有一个常量描述。 第3个常量声明中有4个常量描述。

在上面的例子中,符号*是一个乘法运算符, 符号!是一个布尔取否运算符。 运算符将在下一篇文章中详述。

常量声明中的等号=表示“绑定”而非“赋值”。 每个常量描述将一个或多个字面值绑定到各自对应的有名常量上。 或者说,每个有名常量其实代表着一个字面常量。

在上面的例子中,有名常量πPi都绑定到(或者说代表着)字面常量3.1416。 这两个有名变量可以在程序代码中被多次使用,从而有效避免了字面常量3.1416在代码中出现在多处。 如果字面常量3.1416在代码中出现在多处, 当我们以后欲将3.1416改为3.14的时候,所有出现在代码中的3.1416都得逐个修改。 有了有名变量的帮助,我们只需修改对应常量描述中的3.1416即可。 这是常量声明的主要作用。当然常量声明也可常常增加代码的可读性(代码即注释)。

以后,我们使用非常量这一术语表示不是常量的值。 下一节将要介绍的变量就属于非常量。

注意,常量可以直接声明在包中,也可以声明在函数体中。 声明在函数体中的常量称为局部常量(local constant),直接声明在包中的常量称为包级常量(package-level constant)。 包级常量也常常被称为全局常量。

包级常量声明中的常量描述的顺序并不重要。比如在上面的例子中, 常量描述NoYes的顺序可以掉换一下。

上面例子中声明的所有常量都是类型不确定的。 它们各自的默认类型和它们各自代表的字面值的默认类型是一样的。

类型确定有名变量

我们可以在声明一些常量的时候指定这些常量的确切类型。 这样声明的常量称为类型确定有名常量。 在下面这个例子中,所有这4个声明的常量都是类型确定的。 XY的类型都是float32AB的类型都是int64
const X float32 = 3.14

const (
	A, B int64   = -3, 5
	Y    float32 = 2.718
)

如果一个常量描述中包含多个类型确定常量,则这些常量的类型必然是一样的, 比如上例中的AB

我们也可以使用显式类型转换来声明类型确定常量。 下面的例子和上面的例子是完全等价的。
const X = float32(3.14)

const (
	A, B = int64(-3), int64(5)
	Y    = float32(2.718)
)

欲将一个字面常量绑定到一个类型确定有名常量上,此字面常量必须能够表示为此常量的确定类型的值。 否则,编译将报错。
const a uint8 = 256             // error: 256溢出uint8
const b = uint8(255) + uint8(1) // error: 256溢出uint8
const c = int8(-128) / int8(-1) // error: 128溢出int8
const MaxUint_a = uint(^0)      // error: -1溢出uint
const MaxUint_b uint = ^0       // error: -1溢出uint

在上面的例子中,符号^为位反运算符,符号+为加法运算符,符号/为除法运算符。

下面这个类型确定常量声明在64位的操作系统上是合法的,但在32位的操作系统上是非法的。 因为一个uint值在32位操作系统上的尺寸是32位, (1 << 64) - 1将溢出uint。(这里,符号<<为左移位运算符。)
const MaxUint uint = (1 << 64) - 1

那么如何声明一个代表着最大uint值的常量呢? 我们可以用下面这个常量声明来替换上面这个。下面这个声明在在64位和32位的操作系统上都是合法的。
const MaxUint = ^uint(0)

类似地,我们可以使用下面这个常量声明来声明一个有名常量来表示最大的int值。(这里,符号>>为右移位运算符。)
const MaxInt = int(^uint(0) >> 1)

使用类似的方法,我们可以声明一个常量来表示当前操作系统的位数,或者检查当前操作系统是32位的还是64位的。
const NativeWordBits = 32 << (^uint(0) >> 63) // 64 or 32
const Is64bitOS = ^uint(0) >> 63 != 0
const Is32bitOS = ^uint(0) >> 32 == 0

这里,符号!===分别为不等于和等于比较运算符。

常量声明中的自动补全

在一个包含多个常量描述的常量声明中,除了第一个常量描述,其它后续的常量描述都可以只有标识符部分。 Go编译器将通过照抄前面最紧挨的一个完整的常量描述来自动补全不完整的常量描述。 比如,在编译阶段,编译器会将下面的代码
const (
	X float32 = 3.14
	Y                // 这里必须只有一个标识符
	Z                // 这里必须只有一个标识符

	A, B = "Go", "language"
	C, _
	// 上一行中的空标识符是必需的(如果
	// 上一行是一个不完整的常量描述)。
)
自动补全为
const (
	X float32 = 3.14
	Y float32 = 3.14
	Z float32 = 3.14

	A, B = "Go", "language"
	C, _ = "Go", "language"
)

在常量声明中使用iota

iota是Go中预声明(内置)的一个特殊的有名常量。 iota被预声明为0,但是它的值在编译阶段并非恒定。 当此预声明的iota出现在一个常量声明中的时候,它的值在第n个常量描述中的值为n(从0开始)。 所以iota只对含有多个常量描述的常量声明有意义。

iota和常量描述自动补全相结合有的时候能够给Go编程带来很大便利。 比如,下面是一个使用了这两个特性的例子。 请阅读代码注释以了解清楚各个常量被绑定的值。
package main

func main() {
	const (
		k = 3 // 在此处,iota == 0

		m float32 = iota + .5 // m float32 = 1 + .5
		n                     // n float32 = 2 + .5

		p = 9             // 在此处,iota == 3
		q = iota * 2      // q = 4 * 2
		_                 // _ = 5 * 2
		r                 // r = 6 * 2
		s, t = iota, iota // s, t = 7, 7
		u, v              // u, v = 8, 8
		_, w              // _, w = 9, 9
	)

	const x = iota // x = 0 (iota == 0)
	const (
		y = iota // y = 0 (iota == 0)
		z        // z = 1
	)

	println(m)             // +1.500000e+000
	println(n)             // +2.500000e+000
	println(q, r)          // 8 12
	println(s, t, u, v, w) // 7 7 8 8 9
	println(x, y, z)       // 0 0 1
}

上面的例子只是展示了一下如何使用iota。 在实际编程中,我们应该用有意义的方式使用之。比如:
const (
	Failed = iota - 1 // == -1
	Unknown           // == 0
	Succeeded         // == 1
)

const (
	Readable = 1 << iota // == 1
	Writable             // == 2
	Executable           // == 4
)

在上面这段代码中,-是一个减法运算符。

变量声明和赋值操作语句

变量可以被看作是在运行时刻存储在内存中并且可以被更改的有名字的值。

所有的变量值都是类型确定值。当声明一个变量的时候,我们必须在代码中给编译器提供足够的信息来让编译器推断出此变量的确切类型。

在一个函数体内声明的变量称为局部变量。 在任何函数体内声明的变量称为包级或者全局变量。

Go语言有两种变量声明形式。一种称为标准形式,另一种称为短声明形式。 短声明形式只能用来声明局部变量。

标准变量声明形式

每条标准变量声明形式语句起始于一个var关键字。 每个var关键字跟随着一个变量名。 每个变量名必须为一个标识符

下面是几条完整形式的标准变量声明语句。 这些声明确地指定了被声明的变量的类型和初始值。
var lang, website string = "Go", "https://golang.org"
var compiled, dynamic bool = true, false
var announceYear int = 2008

我们可以看到,和常量声明一样,多个同类型的变量可以在一条语句中被声明。

完整形式的标准变量声明使用起来有些罗嗦,因此很少在日常Go编程中使用。 在日常Go编程中,另外两种变种形式用得更广泛一些。 一种变种形式省略了变量类型(但仍指定了变量的初始值),这时编译器将根据初始值的字面形式来推断出变量的类型。 另一种变种形式省略了初始值(但仍指定了变量类型),这时编译器将使用变量类型的零值做为变量的初始值。

下面是一些第一种变种形式的用例。在这些用例中,如果一个初始值是一个类型确定值,则对应声明的变量的类型将被推断为此初始值的类型; 如果一个初始值是一个类型不确定值,则对应声明的变量的类型将被推断为此初始值的默认类型。 注意在这种变种中,同时声明的多个变量的类型可以不一样。
// 变量lang和dynamic的类型将被推断为内置类型string和bool。
var lang, dynamic = "Go", false

// 变量compiled和announceYear的类型将被推断
// 为内置类型bool和int。
var compiled, announceYear = true, 2009

// 变量website的类型将被推断为内置类型string。
var website = "https://golang.org"

上例中的类型推断可以被视为隐式类型转换。

下例展示了几个省略了初始值的标准变量声明。每个声明的变量的初始值为它们各自的类型的零值。
var lang, website string      // 两者都被初始化为空字符串。
var interpreted, dynamic bool // 两者都被初始化为false。
var n int                     // 被初始化为0。

和常量声明一样,多个变量可以用一对小括号组团在一起被声明。
var (
	lang, bornYear, compiled     = "Go", 2007, true
	announceAt, releaseAt    int = 2009, 2012
	createdBy, website       string
)

上面这个变量声明语句已经被go fmt命令格式化过了。 这个变量声明语句包含三个变量描述(variable specification)。

一般来说,将多个相关的变量声明在一起将增强代码的可读性。

纯赋值语句

在上面展示的变量声明的例子中,等号=表示赋值。 一旦一个变量被声明之后,它的值可以被通过纯赋值语句来修改。 多个变量可以同时在一条赋值语句中被修改。

一个赋值语句等号左边的表达式必须是一个可寻址的值、一个映射元素或者一个空标识符。 内存地址(以及指针)和映射将在以后的文章中介绍。

常量是不可改变的(不可寻址的),所以常量不能做为目标值出现在纯赋值语句的左边,而只能出现在右边用做源值。 变量既可以出现在纯赋值语句的左边用做目标值,也可以出现在右边用做源值。

空标识符也可以出现在纯赋值语句的左边,表示不关心对应的目标值。 空标识符不可被用做源值。

一个包含了很多(合法或者不合法的)纯赋值语句的例子:
const N = 123
var x int
var y, z float32

N = 789 // error: N是一个不可变量
y = N   // ok: N被隐式转换为类型float32
x = y   // error: 类型不匹配
x = N   // ok: N被隐式转换为类型int
y = x   // error: 类型不匹配
z = y   // ok
_ = y   // ok

z, y = y, z               // ok
_, y = y, z               // ok
z, _ = y, z               // ok
_, _ = y, z               // ok
x, y = 69, 1.23           // ok
x, y = y, x               // error: 类型不匹配
x, y = int(y), float32(x) // ok

上例中的最后一行使用了显式类型转换,否则此赋值(见倒数第二行)将不合法。 数字非常量值的类型转换规则将在后边的章节介绍。

Go不支持某些其它语言中的连等语法。下面的赋值语句在Go中是不合法的。
var a, b int
a = b = 123 // 语法错误

短变量声明形式

我们也可以用短变量声明形式来声明一些局部变量。比如下例:
package main

func main() {
	// 变量lang和year都为新声明的变量。
	lang, year := "Go language", 2007

	// 这里,只有变量createdBy是新声明的变量。
	// 变量year已经在上面声明过了,所以这里仅仅
	// 改变了它的值,或者说它被重新声明了。
	year, createdBy := 2009, "Google Research"

	// 这是一个纯赋值语句。
	lang, year = "Go", 2012

	print(lang, "由", createdBy, "发明")
	print("并发布于", year, "年。")
	println()
}

每个短声明语句中必须至少有一个新声明的变量。

从上面的例子中,我们可以看到短变量声明形式和标准变量声明形式有几个显著的区别:
  1. 短声明形式不包含var关键字,并且不能指定变量的类型。
  2. 短变量声明中的赋值符号必须为:=
  3. 在一个短声明语句的左侧,已经声明过的变量和新声明的变量可以共存。 但在一个标准声明语句中,所有左侧出现在的变量必须都为新声明的变量。

注意,相对于纯赋值语句,目前短声明语句有一个限制:出现在一个短声明左侧的项必须都为纯标识符。 以后我们将学习到在纯赋值语句的左边可以出现结构体值的字段,指针的解引用和容器类型值的元素索引项等。 但是这些项不能出现在一个变量短声明语句的左边。

关于“赋值”这个术语

以后,当“赋值”这个术语被提到的时候,它可以指一个纯赋值、一个短变量声明或者是一个初始值未省略的标准变量声明语句。

y = x是一条合法的赋值语句时,我们可以说x可以被赋给y。 假设y的类型为Ty,有时为了叙述方便,我们也可以说x可以被赋给类型Ty

一般来说,如果x可以被赋给y,则y应该是可修改的,并且xy的类型相同或者x可以被隐式转换到y的类型。 当然,y也可以是空标识符_

每个局部声明的变量至少要被有效使用一次

注意,当使用目前的主流Go编译器编译Go代码时,一个局部变量被声明之后至少要被有效使用一次,否则编译器将报错。 包级变量无此限制。 如果一个变量总是被当作赋值语句中的目标值,那么我们认为这个变量没有被有效使用过。

下面这个例子编译不通过。
package main

var x, y, z = 123, true, "foo" // 包级变量

func main() {
	var q, r = 789, false
	r, s := true, "bar"
	r = y // r没有被有效使用。
	x = q // q被有效使用了。
}
当编译上面这个程序的时候,编译器将报错(这个程序代码存在一个名为example-unused.go的文件中):
./example-unused.go:6:6: r declared and not used
./example-unused.go:7:16: s declared and not used

避免编译器报错的方法很简单,要么删除相关的变量声明,要么像下面这样,将未曾有效使用过的变量(这里是rs)赋给空标识符。
package main

var x, y, z = 123, true, "foo"

func main() {
	var q, r = 789, false
	r, s := true, "bar"
	r = y
	x = q

	_, _ = r, s // 将r和s做为源值使用一次。
}

若干包级变量在声明时刻的依赖关系将影响它们的初始化顺序

下面这个例子中的声明的变量的初始化顺序为y = 5c = yb = c+1a = b+1x = a+1
var x, y = a+1, 5         // 8 5
var a, b, c = b+1, c+1, y // 7 6 5

包级变量在初始化的时候不能相互依赖。比如,下面这个变量声明语句编译不通过。
var x, y = y, x

值的可寻址性

在Go中,有些值是可以被寻址的。上面已经提到所有变量都是可以寻址的,所有常量都是不可被寻址。 我们可以从后面的指针一文了解更多关于内存地址和指针的知识。

非常量数字值相关的显式类型转换规则

在Go中,两个类型不一样的基本类型值是不能相互赋值的。 我们必须使用显式类型转换将一个值转换为另一个值的类型之后才能进行赋值。

前面某节已经提到了整数(不论常量还是非常量)都可以被显式转换为字符串类型。 这里再介绍两个不同类型数字值之间的转换规则。 上面已经提到,常量数字值的类型转换不能溢出。此规则不适用于非常量数字值的类型转换。 非常量数字值的类型转换中,溢出是允许的。 另外当将一个浮点数非常量值(比如一个变量)转换为一个整数类型的时候,舍入(或者精度丢失)也是允许的。 具体规则如下: 在下面的例子中,第7行和第15行的隐式转换是不允许的,第5行和第14行的显式转换也是不允许的。
const a = -1.23
// 变量b的类型被推断为内置类型float64。
var b = a
// error: 常量1.23不能被截断舍入到一个整数。
var x = int32(a)
// error: float64类型值不能被隐式转换到int32。
var y int32 = b
// ok: z == -1,变量z的类型被推断为int32。
//     z的小数部分将被舍弃。
var z = int32(b)

const k int16 = 255
var n = k            // 变量n的类型将被推断为int16。
var f = uint8(k + 1) // error: 常量256溢出了uint8。
var g uint8 = n + 1  // error: int16值不能隐式转换为uint8。
var h = uint8(n + 1) // ok: h == 0,变量h的类型为uint8。
                     // (n+1)溢出uint8,所以只有低8位
                     // bits(都为0)被保留。

3行和第13行的隐式转换中,ak被转换为它们各自的默认类型。

变量和常量的作用域

在Go中,我们可以使用一对大括号来显式形成一个(局部)代码块。一个代码块可以内嵌另一个代码块。 最外层的代码块称为包级代码块。 一个声明在一个内层代码块中的常量或者变量将遮挡另一个外层代码块中声明的同名变量或者常量。 比如,下面的代码中声明了3个名为x的变量。 内层的x将遮挡外层的x, 从而外层的x在内层的x声明之后在内层中将不可见。
package main

const y = 70
var x int = 123 // 包级变量

func main() {
	// 此x变量遮挡了包级变量x。
	var x = true

	// 一个内嵌代码块。
	{
		x, y := x, y-10 // 这里,左边的x和y均为新声明
		                // 的变量。右边的x为外层声明的
		                // bool变量。右边的y为包级变量。

		// 在此内层代码块中,从此开始,
		// 刚声明的x和y将遮挡外层声明x和y。

		x, z := !x, y/10 // z是一个新声明的变量。
		                 // x和y是上一句中声明的变量。
		println(x, y, z) // false 60 6
	}
	println(x) // true
	println(y) // 70 (包级变量y从未修改)
	/*
	println(z) // error: z未定义。
	           // z的作用域仅限于上面的最内层代码块。
	*/
}

刚提到的作用域是指一个标识符的可见范围。 一个包级变量或者常量的作用域为其所处于的整个代码包。 一个局部变量或者常量的作用域开始于此变量或者常量的声明的下一行,结束于最内层包含此变量或者常量的声明语句的代码块的结尾。 这是为什么上例中的println(z)将编译不通过。

后面的代码块和作用域一文将详述代码块和标识符的作用域。

更多关于常量声明

一个类型不确定常量所表示的值在可以溢出其默认类型

比如,下例中的三个类型不确定常量均溢出了它们各自的默认类型,但是此程序编译和运行都没问题。
package main

// 三个类型不确定常量。
const n = 1 << 64          // 默认类型为int
const r = 'a' + 0x7FFFFFFF // 默认类型为rune
const x = 2e+308           // 默认类型为float64

func main() {
	_ = n >> 2
	_ = r - 0x7FFFFFFF
	_ = x / 2
}

但是下面这个程序编译不通过,因为三个声明的常量为类型确定常量。
package main

// 三个类型确定常量。
const n int = 1 << 64           // error: 溢出int
const r rune = 'a' + 0x7FFFFFFF // error: 溢出rune
const x float64 = 2e+308        // error: 溢出float64

func main() {}

每个常量标识符将在编译的时候被其绑定的字面值所替代

常量声明可以看作是增强型的C语言中的#define宏。 在编译阶段,所有的标识符将被它们各自绑定的字面值所替代。

如果一个运算中的所有运算数都为常量,则此运算的结果也为常量。或者说,此运算将在编译阶段就被估值。 下一篇文章将介绍Go中的常用运算符

一个例子:
package main

const X = 3
const Y = X + X
var a = X

func main() {
	b := Y
	println(a, b, X, Y)
}
上面这段程序代码将在编译阶段被重写为下面这样:
package main

var a = 3

func main() {
	b := 6
	println(a, b, 3, 6)
}

Go语言101项目目前同时托管在GithubGitlab上。 欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏