1.4 常量与变量

1.4 常量与变量 #

Declarations and scope

declaration binds a non- blank identifier to a  constanttypetype parametervariablefunctionlabel, or  package. Every identifier in a program must be declared. No identifier may be declared twice in the same block, and no identifier may be declared in both the file and package block.

一个声明将一个非空标识符(命名)绑定到一个常量const、类型type、类型参数、变量var、函数func、标签或包package上。程序中的每个标识符都必须被声明。同一个块中不能声明两次同一个标识符,而且标识符不能同时在文件块和包块中被声明。

当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行(所以内层词法域的i能覆盖外层的同名的名字),如果查找失败,则报告“未声明的名字”这样的错误。

一、常量constants #

常量 #

  • numeric constants**数值常量:rune、int、float64、complex128
  • boolean constants:布尔真值由预声明的常量 true 和 false 表示。 预声明的标识符 iota 表示整数常量

字面常量 true 、 false 、 iota 以及仅包含不带类型常量操作数的某些常量表达式是不带类型的。

常量可以通过常量声明或类型转换显式地指定类型,也可以在变量声明、赋值语句或表达式中的操作数中隐式指定。

一个未命名的常量有一个默认类型,这个类型是常量在需要类型化值的环境中隐式转换的类型,例如在 i := 0 这样的简短变量声明中,没有显式的类型。未命名常量的默认类型分别是 bool 、 rune 、 int 、 float64 、 complex128 或 string ,具体取决于它是布尔值、rune、整数、浮点数、复数还是字符串常量。

常量声明 #

将一组标识符(常量的名称)绑定到一组常量表达式的值。

常量在编译时就确定了值,编译器直接替换使用常量的地方为对应值,不分配运行时内存。

1
2
3
4
5
const IPv4Len = 4

func parseIPv4(s string) IP {
    var p [IPv4Len]byte  // 编译器会替换为 [4]byte
}

定义常量的表达式必须也是可被编译器求值的常量表达式。如 1<<3 就是一个常量表达式,而 math.Sin(math.Pi/4) 则不是,因为对 **math.Sin** 的****函数调用在运行时才会发生;常量运算如整数除零、字符串索引越界、任何导致无效浮点数的操作等运行时错误可以在编译时发现;

Go中的常量就是不变量(值不可修改),这样可以防止在运行期被意外或恶意的修改,类型只能是numeric、boolean、string/rune

1
2
3
4
5
const (
	E   = 2.71828182845904523536028747135266249775724709369995957496696763 // https://oeis.org/A001113
	Pi  = 3.14159265358979323846264338327950288419716939937510582097494459 // https://oeis.org/A000796
	Phi = 1.61803398874989484820458683436563811772030917980576286213544862 // https://oeis.org/A001622
)

如果是批量声明的常量,除了第一个外,可省略常量右边的初始化表达式表示使用前面常量的初始化表达式写法和常量类型;可省略类型,由初始化表达式推断;

1
2
3
4
5
6
const (
    a = 1
    b      // 1
    c = 2
    d     // 2
)

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,:len、cap、real、imag、complex和unsafe.Sizeof返回的结果为常量

iota 常量int枚举器 #

预声明标识符 iota 表连续的未类型化整数常量,表示从0开始自动加1,用于构建一组相关的常量。所以Sunday=0,Monday=1,以此类推。只能在常量的表达式中使用。

由于iota可为表达式的一部分,而表达式可以被隐式地重复,所以可用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 首先定义一个Weekday命名类型,然后为一周的每天定义了一个常量,从周日0开始。
type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 每个常量都是1024的幂
// x<<n左移运算 等价于 乘以 2^n,**用0填充右边空缺的bit位**
const (
	_   = 1 << (10 * iota)  // 通过赋予空白标识符来忽略第一个值
	
	KiB      // 1024   , 1 << (10 * 1)
	MiB     // 1048576 , 1 << (10 * 2)
	GiB     // 1073741824, 1 << (10 * 3)
	TiB     // 1099511627776, 1 << (10 * 4)   (exceeds 1 << 32)
	PiB     // 1125899906842624, 1 << (10 * 5)
	EiB     // 1152921504606846976, 1 << (10 * 6)
	ZiB     // ZiB和YiB的值已经超出int64,但是它们依然是合法的常量  1180591620717411303424, 1 << (10 * 7)    (exceeds 1 << 64) 溢出
	YiB     // 1208925819614629174706176, 1 << (10 * 8) 溢出

	fmt.Println(YiB/ZiB) // "1024"    // 而且像下面的常量表达式依然有效(译注:YiB/ZiB是在编译期计算出来的,并且结果常量是1024,是Go语言int变量能有效表示的):
)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 给一个无符号整数的最低5bit的每个bit指定一个名字
// 使用这些常量可以用于测试、设置或清除对应的bit位的值
type Flags uint

const (
	FlagUp           Flags = 1 << iota // 1 << 0,1 * 2^0
	FlagBroadcast                      // 1 << 1,1 * 2^1
	FlagLoopback                       // 1 << 2,1 * 2^2
	FlagPointToPoint                   // 1 << 3,1 * 2^3
	FlagMulticast                      // 1 << 4,1 * 2^4
)

Go中没有设计内置的枚举类型enum,而是通过 类型定义+const常量组+iota(可选) 实现枚举 , 保持了语言的简洁性和设计的一致性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Go 的哲学:用现有的简单机制解决问题
// uint8枚举
type FormatType uint8

const (
	FString FormatType = 0
	GoTemplate FormatType = 1
	Jinja2 FormatType = 2
)

// 语义枚举:string枚举
type RoleType string

const (
    Assistant RoleType = "assistant"  // AI助手
    User      RoleType = "user"       // 用户
    System    RoleType = "system"     // 系统
    Tool      RoleType = "tool"       // 工具
)

// 而不是引入新的语法
// enum RoleType { Assistant, User, System, Tool }  // 这种语法 Go 没有

由于可将 String 之类的方法附加在用户定义的类型上(如结构体), 因此它就为打印时自动格式化任意值提供了可能性。 尽管你常常会看到这种技术应用于结构体,但它对于像 ByteSize 之类的浮点数标量等类型也是有用的。

表达式 YB 会打印出 1.00YB,而 ByteSize(1e13) 则会打印出 9.09

在这里用 Sprintf 实现 ByteSize 的 String 方法很安全(不会无限递归),这倒不是因为类型转换,而是它以 %f 调用了 Sprintf,它并不是一种字符串格式:Sprintf 只会在它需要字符串时才调用 String 方法,而 %f 需要一个浮点数值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

无类型常量 #

Go中6种未明确类型的常量类型,编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算(你可以认为至少有256bit的运算精度

  • 无类型的布尔型
  • 无类型的整数
  • 无类型的字符
  • 无类型的浮点数
  • 无类型的复数
  • 无类型的字符串 通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。
 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
// math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

// 对于常量面值,不同的写法可能会对应不同的类型。
// 如0、0.0、0i和\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。
// true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。
var f float64 = 212
fmt.Println((f - 32) * 5 / 9)     // "100"; (f - 32) * 5 is a float64
fmt.Println(5 / 9 * (f - 32))     // "0";   5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float

// 只有常量可以是无类型的。
// 当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。
var f float64 = 3 + 0i // untyped complex -> float64
f = 2                  // untyped integer -> float64
f = 1e123              // untyped floating-point -> float64
f = 'a'                // untyped rune -> float64

// 上面的语句相当于:
var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a'

// 无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。
// 对于浮点数和复数,可能会有舍入处理:
const (
    deadbeef = 0xdeadbeef // untyped int with value 3735928559
    a = uint32(deadbeef)  // uint32 with value 3735928559
    b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
    c = float64(deadbeef) // float64 with value 3735928559 (exact)
    d = int32(deadbeef)   // compile error: constant overflows int32
    e = float64(1e309)    // compile error: constant overflows float64
    f = uint(-1)          // compile error: constant underflows uint
)

// 对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型
i := 0      // untyped integer;        implicit int(0)
r := '\000' // untyped rune;           implicit rune('\000')
f := 0.0    // untyped floating-point; implicit float64(0.0)
c := 0i     // untyped complex;        implicit complex128(0i)

// 注意有一点不同:无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。 
// 如果不知道浮点数类型的内存大小是很难写出正确的数值算法的,因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型。

// 如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型,像下面例子这样:
var i = int8(0)
var i int8 = 0

// 当尝试将这些无类型的常量转为一个接口值时,这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。
fmt.Printf("%T\n", 0)      // "int"
fmt.Printf("%T\n", 0.0)    // "float64"
fmt.Printf("%T\n", 0i)     // "complex128"
fmt.Printf("%T\n", '\000') // "int32" (rune)

二、变量variable #

A variable is a storage location for holding a value. The set of permissible values is determined by the variable’s  type.

变量是用于存储值的存储位置(分配内存)。允许的值集由变量的类型决定。

A variable’s value is retrieved by referring to the variable in an  expression; it is the most recent value  assigned to the variable. If a variable has not yet been assigned a value, its value is the  zero value for its type.

变量的值是通过在表达式中引用该变量来获取的;它是最近分配给该变量的值。如果一个变量尚未被赋值,则其值为该类型的零值。

变量声明 #

一个变量声明创建一个或多个变量,将相应的标识符绑定到它们,并为每个变量指定一个类型和初始值。

var变量声明:var 变量名字 类型 = 表达式。可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。

var通常用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var (
	home   = os.Getenv("HOME")
	user   = os.Getenv("USER")
	gopath = os.Getenv("GOPATH")
)

var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string

var s string
fmt.Println(s) // ""  

var f, err = os.Open(name) // os.Open returns a file and an error

var boiling float64 = 100 // a float64
var names []string
var err error
var p Point

局部变量通常使用简短变量声明语句:=的形式:“名字 := 表达式” (注意不要混淆变量赋值操作),通过:=来省略var关键字,通过初始化表达式推导来省略类型,简洁和灵活;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
i := 100                  // an int
t := 0.0

// 同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语句的循环的初始化语句部分。
i, j := 0, 1

// 交换 i 和 j 的值
i, j = j, i 

简短变量声明:=语句中必须至少要声明一个新的变量(否则编译报错:no new variables on left side of :=)

比较微妙的是,在此基础上允许有作用域范围内已经声明过的变量(此时只有赋值行为,不再有声明行为,不是重复声明)

1
2
3
	username := "xiaoming"
	username, password := "xiaoming", "123456"
	fmt.Printf("username: %s, password: %s\n", username, password)

零值初始化机制 #

省略类型将**根据初始化表达式来推导,省略初始化表达式将根据类型推导其对应的****零值,**初始化表达式可以是字面量或任意的表达式

在Go语言中不存在未初始化的变量,这个特性简化了很多代码,而且在没有增加额外工作的前提下确保边界条件下的合理行为(而不是导致错误或产生不可预知的行为)

Go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。

类型 types零值
booleanfalse
numeric:rune、int、float64、complex1280
string“”(空字符串)
interface或引用类型(slice、pointer、map、chan和func)nil
array、struct等聚合类型每个元素或字段都是对应该类型的零值

**初始化顺序:在包级别声明的变量会在main入口函数执行前完成初始化。**局部变量将在声明语句被执行到的时候完成初始化。

Go的声明语法允许成组声明。单个文档注释应介绍一组相关的常量或变量。由于是整体声明,这种注释往往较为笼统。

1
2
3
4
5
6
7
// 表达式解析失败后返回错误代码。
var (
	ErrInternal      = errors.New("regexp: internal error")
	ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
	ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
	...
)

即便是对于私有名称,也可通过成组声明来表明各项间的关系,例如某一组由互斥体保护的变量。

1
2
3
4
5
6
var (
	countLock   sync.Mutex
	inputCount  uint32
	outputCount uint32
	errorCount  uint32
)

变量的生命周期与作用域 #

变量的生命周期指:在程序运行期间变量有效存在的时间段;虽然Go的GC不再需要Go程序员显式地分配和释放内存,但要编写高效的程序你需要了解变量的生命周期基本原理

Go自动垃圾回收器的基本实现思路:每个包级的变量和每个当前运行函数的每一个局部变量开始通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,定义为垃圾,进行回收。(类似Java垃圾回收算法中的可达性树法)

包一级声明的变量的生命周期和整个程序的运行周期是一致的;(所以滥用全局变量会导致不必要的长期内存占用),如将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止(可达性树法),之后变量的存储空间被gc回收

  • 函数的参数变量和返回值变量都是局部变量,它们在函数每次被调用的时候创建。(所以传指针能节省内存开销);
  • 因为一个变量的有效周期只取决于是否可达所以一个循环迭代/函数体内部的局部变量的生命周期可能超出其局部作用域,局部变量可能在函数返回之后依然存在。(如return 局部变量地址)。逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。(不会被gc)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var global *int

func f() {
    var x int
    x = 1
    // f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,**这个x局部变量从函数f中逃逸了**。
    global = &x
}

func g() {
    // 当g函数返回时,变量*y将是不可达的,可以马上被回收的。因此,*y并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间)。
    y := new(int)
    *y = 1
}
1
2
3
4
5
6
7
8
for t := 0.0; t < cycles*2*math.Pi; t += res {
    x := math.Sin(t)
    y := math.Sin(t*freq + phase)
    img.SetColorIndex(
        size+int(x*size+0.5), size+int(y*size+0.5),
        blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
    )               // 小括弧另起一行缩进,和大括弧的风格保存一致
}

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,(内置的new函数仅仅是个var声明的语法糖,不是新语法)。

声明语句的作用域:源代码中可以有效使用这个名字的范围,对应的是一个源代码的文本区域,是一个编译时的属性;而变量的生命周期是指程序运行时变量存活的有效时间段,在此间区域内它可以被程序的其他部分引用;声明语句对应的词法域决定了作用域范围的大小

  • 对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。
  • 任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。
  • 对于导入的包,例如tempconv导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包。
  • 还有许多声明语句,比如tempconv.CToF函数中的变量c,则是局部作用域的,它只能在函数内部(甚至只能是局部的某些部分)访问。
  • 控制流标号 break、continue、goto语句后面跟着的那种标号,则是函数级的作用域

句法块/显式书写的词法块:由花括弧所包含的一系列语句(如函数体或循环体),这个块决定了内部声明的名字的作用域范围是无法被外部块访问的;

Go is lexically scoped using  blocks:Go 使用块进行词法作用域:

  1. The scope of a  predeclared identifier is the universe block. 预声明标识符的作用域是全局块。
  2. The scope of an identifier denoting a constant, type, variable, or function (but not method) declared at top level (outside any function) is the package block. 在顶层(任何函数外部)声明的常量、类型、变量或函数(非方法)标识符的作用域是包块。
  3. The scope of the package name of an imported package is the file block of the file containing the import declaration. 导入包的包名的作用域是包含导入声明的文件块。
  4. The scope of an identifier denoting a method receiver, function parameter, or result variable is the function body. 标识符表示方法接收者、函数参数或结果变量的作用域是函数体。
  5. The scope of an identifier denoting a type parameter of a function or declared by a method receiver begins after the name of the function and ends at the end of the function body. 标识符表示函数的类型参数或由方法接收器声明的范围从函数名称之后开始,到函数体结束。
  6. The scope of an identifier denoting a type parameter of a type begins after the name of the type and ends at the end of the TypeSpec. 标识符表示类型参数的范围从类型名称之后开始,到 TypeSpec 的末尾结束。
  7. The scope of a constant or variable identifier declared inside a function begins at the end of the ConstSpec or VarSpec (ShortVarDecl for short variable declarations) and ends at the end of the innermost containing block. 在函数内部声明的常量或变量标识符的范围从 ConstSpec 或 VarSpec(对于简短的变量声明,即 ShortVarDecl)的末尾开始,到最内层包含块的末尾结束。
  8. The scope of a type identifier declared inside a function begins at the identifier in the TypeSpec and ends at the end of the innermost containing block. 在函数内部声明的类型标识符的范围从 TypeSpec 中的标识符开始,到最内层包含块的末尾结束。

词法块/隐式的词法块:块(block)的概念推广 并未显式地使用花括号包裹起来的声明代码;对全局的源代码来说,存在一个整体的词法块,称为全局词法块;如每个包、每个for、if和switch语句、每个switch或select的分支也都有有独立的词法块;

一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。例如,你可以声明一个局部变量,和包级的变量同名。或者将一个函数参数的名字声明为new,虽然内置的new是全局作用域的。但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func f() {}

var g = "g"

func main() {
    f := "f"
    fmt.Println(f) // "f"; local var f shadows package-level func f
    fmt.Println(g) // "g"; package-level var
    fmt.Println(h) // compile error: undefined: h
}

在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// if、switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后是每个分支的词法域。

// 在条件部分创建隐式词法域的x
if x := f(); x == 0 {
    fmt.Println(x)
} else if y := g(x); x == y {  // 第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。
    fmt.Println(x, y)
} else {
    fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
	// 1. 在函数体词法域的x
    x := "hello"
		// 2. for初始化词法域的x,隐式的
    for _, x := range x {
				 // 3. for循环体词法域的x
        x := x + 'A' - 'a'
        fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
		// 1. 在函数体词法域的x
    x := "hello!"
		// 4. 条件测试部分法域的x;
    for i := 0; i < len(x); i++ {
				// 2. for的循环体部分的词法域x
        x := x[i]
        if x != '!' {
						// 3. if条件判断的词法域x
            x := x + 'A' - 'a'
            fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
        }
    }
}

包级别的声明的顺序并不会影响作用域范围

  • 包声明、类型声明、函数声明可以引用它自身或者是引用后面的一个声明、相互嵌套或递归;
  • 但变量声明、常量声明不能递归引用自身;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
if f, err := os.Open(fname); err != nil { // f的词法域在if语句内,compile error: unused: f
    return err
}
f.ReadByte() // compile error: undefined f
f.Close()    // compile error: undefined f


// 你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题,但这不是Go语言推荐的做法;
if f, err := os.Open(fname); err != nil {
    return err
} else {
    // f and err are visible here too
    f.ReadByte()
    f.Close()
}

Go语言的风格是在if中处理错误然后直接返回确保正常执行的语句不被代码缩进;

1
2
3
4
5
6
f, err := os.Open(fname)
if err != nil {
    return err
}
f.ReadByte()
f.Close()

要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这本来可以通过直接调用os.Getwd完成,但是将这个从主逻辑中分离出来可能会更好,特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息,然后调用os.Exit(1)终止程序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用:=的简短声明方式:
var cwd string

func init() {
    var err error
    cwd, err = os.Getwd()
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}
 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
package main

import (
	"log"
	"os"
)

var cwd string

func init() {
	cwd, err := os.Getwd() // NOTE: wrong!
	if err != nil {
		log.Fatalf("os.Getwd failed: %v\n", err)
	}
	// 由于当前的编译器会检测到局部声明的cwd并没有使用,然后报告这可能是一个错误,但是这种检测并不可靠。
	// 因为一些小的代码变更(如增加一个局部cwd的打印语句),就导致了这种检测失效(**包级别词法域的cwd没有被使用,但未被编译器检测到**)。
  // 全局的cwd变量依然是没有被正确初始化的,而且**看似正常的日志输出更是让这个BUG更加隐晦**。
	log.Printf("Working directory = %s", cwd)
}

func main() {
	
}


// 2024/12/18 19:26:57 Working directory = /Users/xxx/gopher.run/src/ch2/11.domain
1
2
3
4
5
6
7
8
9

var cwd string

func init() {
    cwd, err := os.Getwd() // compile error: unused: cwd。函数体外的包级别词法域的cwd没有被使用。内部声明的cwd将屏蔽外部的声明。
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

三、类型和值 types and values 的属性 #

值的表示 #

Values of predeclared types (see below for the interfaces any and error), arrays, and structs are self-contained: Each such value contains a complete copy of all its data, and  variables of such types store the entire value. For instance, an array variable provides the storage (the variables) for all elements of the array. The respective  zero values are specific to the value’s types; they are never nil.

预声明类型(见下文关于接口 any 和 error 的说明)、数组和结构体的值是自包含的:每个这样的值包含其所有数据的完整副本,而此类类型的变量存储整个值。例如,数组变量提供了数组所有元素的存储(变量)。相应的零值是特定于值类型的;它们永远不会是 nil 。

Non-nil pointer, function, slice, map, and channel values contain references to underlying data which may be shared by multiple values:

非空的指针、函数、切片、映射和通道值包含对底层数据的引用,这些数据可能被多个值共享:

  • A pointer value is a reference to the variable holding the pointer base type value. 指针值是指向持有指针基本类型值的变量的引用。
  • A function value contains references to the (possibly  anonymous) function and enclosed variables. 函数值包含对(可能是匿名的)函数及其封闭变量的引用。
  • A slice value contains the slice length, capacity, and a reference to its  underlying array. 切片值包含切片长度、容量及其底层数组的引用。
  • A map or channel value is a reference to the implementation-specific data structure of the map or channel. 映射或通道值是指向映射或通道的特定于实现的数组的引用。 An interface value may be self-contained or contain references to underlying data depending on the interface’s  dynamic type. The predeclared identifier nil is the zero value for types whose values can contain references.

接口值可能包含自身数据或包含对底层数据的引用,这取决于接口的动态类型。预声明的标识符 nil 是那些值可以包含引用的类型为零值。

When multiple values share underlying data, changing one value may change another. For instance, changing an element of a  slice will change that element in the underlying array for all slices that share the array.

当多个值共享底层数据时,改变一个值可能会改变另一个值。例如,改变切片的一个元素会改变所有共享该数组的切片中的该元素。

赋值 #

源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
x = 1                       // 命名变量的赋值
*p = true                   // 通过指针间接赋值
person.name = "bob"         // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值
// 特定的二元算术运算符和赋值语句的复合操作的简洁形式,**可以省去对变量表达式的重复计算**
count[x] *= scale

v := 1
v++    // ++自增语句,不是表达式,表达式指出现在赋值语句右边的。等价方式 v = v + 1;v 变成 2
v--    // --自减语句,不是表达式。等价方式 v = v - 1;v 变成 1

元组赋值

元组赋值允许同时更新多个变量的值。先进行所有表达式求值(赋值语句右边的),再统一赋值更新左边对应变量的值。例如我们可以这样交换两个变量的值:

源码:

1
2
3
4
// 对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助
x, y := 1, 2
y, x = x, y
fmt.Println(x, y) // 2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// gcd, Greatest Common Divisor 最大公约数,欧几里德的GCD是最早的非平凡算法
package main

import "fmt"

func main() {
	fmt.Println(gcd(12, 18))
}

// 同 var变量申明,类型在后
func gcd(x, y int) int {
	for y != 0 {
		x, y = y, x%y
	}
	return x
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// fib,Fibonacci,计算斐波纳契数列的第N个数。
package main

import "fmt"

func main() {
	fmt.Println(fib(10))
}

// 参数列表(入参)、返回值列表(出参)
func fib(n int) (int, []int) {
	fib := []int{}
	x, y := 0, 1
	// 初始值第一个、第二个数都是1,后续每个数都是前两个数之和
	for i := 0; i < n; i++ {
		x, y = y, x+y
		fib = append(fib, x)
	}
	return x, fib
}
1
2
3
// 元组赋值也可以使一系列琐碎赋值更加紧凑(译注: 特别是在for循环的初始化部分)。
// 但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。
i, j, k = 2, 3, 5
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 有些表达式会产生多个值

// 额外的返回值表达某种错误类型 err,调用一个有多个返回值的函数
f, err = os.Open("foo.txt") // function call returns two values

// 额外的返回布尔值,ok,表示操作是否成功。
v, ok = m[key]             // map查找(map lookup)。
v, ok = x.(T)              // 类型断言(type assertion)。
v, ok = <-ch               // 通道接收(channel receive)。


// 以下并不一定是产生两个结果,也可能只产生一个结果。

v = m[key]                // map查找,失败时返回零值
v = x.(T)                 // type断言,失败时panic异常
v = <-ch                  // 管道接收,失败时返回零值(阻塞不算是失败)

和变量声明一样,我们可以**用下划线空白标识符_来丢弃不需要的值**_, ok = m[key]            // map返回2个值
_, ok = mm[""], false     // map返回1个值
_ = mm[""]                // map返回1个值

_, err = io.Copy(dst, src) // 丢弃字节数
_, ok = x.(T)              // 只检测类型,忽略具体值

可赋值性 #

赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为。函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句会隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量也会产生赋值行为。如:

1
2
3
4
5
6
7
8
// **一个复合类型的字面量也会产生赋值行为**
medals := []string{"gold", "silver", "bronze"}
// 隐式地对slice的每个元素进行赋值操作,类似这样写的行为:
medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"

// map和chan的元素,虽然不是普通的变量,但是也有类似的隐式赋值行为。

只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的 (左右为相同的数据类型)。

可赋值性的规则对于不同类型有着不同要求,对每个新类型特殊的地方我们会专门解释。对于目前我们已经讨论过的类型,它的规则是简单的:类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。常量则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。

对于两个值是否可以用==!=进行相****等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。每个新类型比较特殊的地方后续做专门的解释。

四、类型声明 Type #

A type declaration binds an identifier, the type name, to a  type. Type declarations come in two forms: alias declarations and type definitions.

类型声明将一个标识符,即类型名称,绑定到一个类型。类型声明有两种形式:别名声明和类型定义。

别名声明 Alias declarations #

一个别名声明将标识符绑定到给定的类型 [ Go 1.9]。

在其标识符的作用范围内,它作为给定类型的别名。

1
2
3
4
type (
	nodeList = []*Node  // nodeList and []*Node are identical types
	Polar    = polar    // Polar and polar denote identical types
)

如果别名声明指定了类型参数[ Go 1.24],则类型名称表示一个泛型别名。泛型别名在使用时必须实例化。

1
type set[P comparable] = map[P]bool

类型定义 Type definitions #

一个类型定义创建一个新的、不同的类型,其具有与给定类型相同的底层类型和操作,并绑定一个标识符(即类型名)

变量或表达式的类型,定义了对应存储值的属性特征(如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的是否支持一些操作符,以及它们自己关联的方法集等)

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。(译注:对于中文汉字,Unicode标志都作为小写字母处理,因此中文的命名默认不能导出。不过国内的用户针对该问题提出了不同的看法,根据RobPike的回复,在Go2中有可能会将中日韩等字符当作大写字母处理。下面是RobPik在  Issue763 的回复);

这个新类型称为定义类型。它与任何其他类型都不同,包括它所创建自的类型。

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

type 类型名字 底层类型 

type TreeNode struct {
	left, right *TreeNode
	value any
}

type (
	Point struct{ x, y float64 }  // Point and struct{ x, y float64 } are different types
	polar Point                   // polar and Point denote different types
)

type Block interface {
	BlockSize() int
	Encrypt(src, dst []byte)
	Decrypt(src, dst []byte)
}

一个已定义的类型可以与其关联方法。它不会继承绑定到给定类型的任何方法,但接口类型的或复合类型的元素的方法集保持不变:

**类型的方法集:**命名类型还可以为该类型的值定义新的行为(一组关联到该类型的函数集合)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 声明语句中Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

// 许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c)   // "100°C"
fmt.Println(c)          // "100°C"
fmt.Printf("%g\n", c)   // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// A Mutex is a data type with two methods, Lock and Unlock.
type Mutex struct         { /* Mutex fields */ }
func (m *Mutex) Lock()    { /* Lock implementation */ }
func (m *Mutex) Unlock()  { /* Unlock implementation */ }

// NewMutex has the same composition as Mutex but its method set is empty.
type NewMutex Mutex

// The method set of PtrMutex's underlying type *Mutex remains unchanged,
// but the method set of PtrMutex is empty.
type PtrMutex *Mutex

// The method set of *PrintableMutex contains the methods
// Lock and Unlock bound to its embedded field Mutex.
type PrintableMutex struct {
	Mutex
}

// MyBlock is an interface type that has the same method set as Block.
type MyBlock Block

类型定义可用于定义不同的布尔型、数值型或字符串类型,并将方法与它们关联起来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type TimeZone int

const (
	EST TimeZone = -(5 + iota)
	CST
	MST
	PST
)

func (tz TimeZone) String() string {
	return fmt.Sprintf("GMT%+dh", tz)
}

如果类型定义指定了类型参数,则类型名称表示一个泛型类型。泛型类型在使用时必须实例化。

1
2
3
4
type List[T any] struct {
	next  *List[T]
	value T
}

一个泛型类型也可以与其关联方法。在这种情况下,方法接收者必须声明与泛型类型定义中相同数量的类型参数。

1
2
// The method Len returns the number of elements in the linked list l.
func (l *List[T]) Len() int  { … }

源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv

import "fmt"

// 刻意区分摄氏、华氏类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;
type Celsius float64    // **类型声明:**摄氏温度类型
type Fahrenheit float64 // **类型声明:**华氏温度类型

const (
    AbsoluteZeroC Celsius = -273.15 // 绝对零度
    FreezingC     Celsius = 0       // 结冰点温度
    BoilingC      Celsius = 100     // 沸水温度
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }  // 温度换算

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

源码解读:

Celsius、Fahrenheit是不同的数据类型,所以不可以被相互比较或混在一个表达式运算。(但底层类型都是float64)

类型转换:对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(限制在两个类型的底层基础类型相同,或两者都是指向相同底层结构的指针类时。译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0))。类型转换(区别于函数调用)不会改变值本身,但是会使它们的语义发生变化

数值类型之间的转型也是允许的,如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为**[]byte**类型的slice将拷贝一个字符串数据的副本。在任何情况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段

底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持

源码:

go test tempconv_test.go*_test.go 为文件名的文件通常是用于编写测试代码的。

 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
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv

import "fmt"

func Example_one() {
	// 计算摄氏温度差

	// 打印 BoilingC(沸点)和 FreezingC(冰点)之间的摄氏温度差
	fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
	// 将 BoilingC 转换为华氏温度
	boilingF := CToF(BoilingC)
	// 打印 boilingF(沸点的华氏温度)和 CToF(FreezingC)(冰点的华氏温度)之间的华氏温度差
	fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F

	// 尝试打印 boilingF(沸点的华氏温度)和 FreezingC(冰点的摄氏温度)之间的温度差,这会导致类型不匹配错误
	// fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
}

func Example_two() {
	//!+printf
	c := FToC(212.0)
	fmt.Println(c.String()) // 打印摄氏温度的字符串表示,"100°C"
	fmt.Printf("%v\n", c)   // %v,自动调用 String 方法。"100°C"; no need to call String explicitly
	fmt.Printf("%s\n", c)   // %s,自动调用 String 方法。"100°C"
	fmt.Println(c)          // 直接打印,自动调用 String 方法。"100°C"
	fmt.Printf("%g\n", c)   // %g,不会调用 String 方法。"100"; does not call String
	fmt.Println(float64(c)) // "100"; does not call String
}
1
2
3
4
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC)       // compile error: type mismatch

比较运算符==<也可以用来比较有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:

1
2
3
4
5
6
7
var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
// 尽管看起来像函数调用,但是Celsius(f)是类型转换操作,它并不会改变值,仅仅是改变值的类型而已(如Java中的(int)s)
fmt.Println(c == Celsius(f)) // "true"! 测试为真的原因是因为c和f都是零值

一个命名的类型可以提供书写方便,特别是可以避免一遍又一遍地书写复杂类型(译注:例如用匿名的结构体定义变量)。对于是复杂的类型(如结构体)将会简洁很多。

五、类型参数声明 #

类型参数列表看起来像普通的函数参数列表,但类型参数名称必须全部存在,且列表用方括号而不是圆括号括起来[ Go 1.18]。

就像每个普通函数参数都有一个参数类型一样,每个类型参数都有一个相应的(元)类型,这个类型被称为它的类型约束。

1
2
3
4
5
[P any]
[S interface{ ~[]byte|string }]
[S ~[]E, E any]
[P Constraint[int]]
[_ any]

类型约束: 是一个接口,它定义了相应类型参数的可接受类型参数集,并控制该类型参数值的支持的操作 [Go 1.18]。

如果约束是一个形式为 interface{E} 的接口字面量,其中 E 是一个嵌入的类型元素(不是方法),在类型参数列表中,为了方便,可以省略外部的 interface{ … } :

1
2
3
4
[T []P]                      // = [T interface{[]P}]
[T ~int]                     // = [T interface{~int}]
[T int|string]               // = [T interface{int|string}]
type Constraint ~int         // illegal: ~int is not in a type parameter list

**满足类型约束:**类型 T 满足约束 C ,如果

  • T 实现 C ;或
  • C 可以写成 interface{ comparable; E } 的形式,其中 E 是一个基本接口, T 是可比较的并且实现 E
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type argument      type constraint                // constraint satisfaction

int                interface{ ~int }              // satisfied: int implements interface{ ~int }
string             comparable                     // satisfied: string implements comparable (string is strictly comparable)
[]byte             comparable                     // not satisfied: slices are not comparable
any                interface{ comparable; int }   // not satisfied: any does not implement interface{ int }
any                comparable                     // satisfied: any is comparable and implements the basic interface any
struct{f any}      comparable                     // satisfied: struct{f any} is comparable and implements the basic interface any
any                interface{ comparable; m() }   // not satisfied: any does not implement the basic interface interface{ m() }
interface{ m() }   interface{ comparable; m() }   // satisfied: interface{ m() } is comparable and implements the basic interface interface{ m() }

六、init函数 #

每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态。 (其实每个文件都可以拥有多个 init 函数。)

而它的结束就意味着初始化结束: 只有该包中的所有变量声明都通过它们的初始化器求值后 init 才会被调用, 而那些 init 只有在所有已导入的包都被初始化后才会被求值。

除了那些不能被表示成声明的初始化外,init 函数还常被用在程序真正开始执行前,检验或校正程序的状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func init() {
	if user == "" {
		log.Fatal("$USER not set")
	}
	if home == "" {
		home = "/home/" + user
	}
	if gopath == "" {
		gopath = home + "/go"
	}
	// gopath 可通过命令行中的 --gopath 标记覆盖掉。
	flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}