笔记根据GO圣经


程序结构

变量

指针

  1. 2.3.2. 指针 指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点,在早些的echo版本中,就包含了两个可选的命令行参数:-n用于忽略行尾的换行符,-s sep用于指定分隔字符(默认是空格)。

new

  1. new另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T
1
2
3
4
5
p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(p) // "0xc00001e218"
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。

语法糖:在编程中,“语法糖”(Syntactic Sugar)是一种语法上的便利特性,它不会引入新的基础概念,但是能够使代码更加简洁、易读或者方便。

 对于Go语言中的new(T)函数来说,确实可以被称为一种语法糖。虽然它看起来像一个函数调用,但它实际上并不创建新的基础概念。它的功能非常简单,只是为指定类型T分配了足够的内存,并返回指向该内存的指针。

 与使用普通的变量声明语句来创建变量相比,new(T)的确省去了为临时变量取名的步骤,但实际上并没有引入任何新的概念。使用new(T)只是一种更为简洁的方式来动态分配内存,返回一个指针,使得代码更加清晰和便于使用。

 这里的"语法糖"一词是用来比喻编程语言中的某种特性,这种特性并不是必需的,也不会引入新的语言规则或语义,而只是为了提高代码的可读性、简洁性或使用便捷性。类似于人们在喝咖啡时可以添加糖来提升口感,但糖本身并非必需的咖啡成分。语法糖是一种让代码更甜的编程技巧。

  1. 在Go语言中,new是一个预定义的函数,而不是关键字。因此,你可以使用new作为变量名来重新定义它,并将其赋予不同的类型。

在你提供的例子中:

1
2
3
func delta(old, new int) int {
return new - old
}

你在函数delta的参数列表中使用了int类型的变量名new,这会导致在函数体内部无法直接使用预定义的new函数。由于函数参数优先级高于预定义函数,函数参数名会覆盖预定义的new函数。

在这种情况下,如果你需要使用预定义的new函数,你可以通过使用包名进行限定来调用它,例如fmt.Println(new(...))。这样做可以明确告诉编译器你想要调用预定义的new函数。

需要注意的是,虽然在Go语言中可以重新定义new作为变量名,但这样的命名并不推荐,因为这样会导致代码可读性下降,并可能造成混淆。通常,我们应该遵循良好的命名规范,避免使用与预定义函数或关键字相同的名称来定义变量或函数名。这有助于保持代码的清晰和易读性。

变量的生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
那么Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。

因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。

1
2
3
4
5
6
7
8
9
10
11
12
var global *int

func f() {
var x int
x = 1
global = &x
}

func g() {
y := new(int)
*y = 1
}

f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量*y将是不可达的,也就是说可以马上被回收的。因此,*y并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

赋值

元组赋值

通常,这类函数会用额外的返回值来表达某种错误类型,例如os.Open是用额外的返回值返回一个error类型的错误,还有一些是用来返回布尔值,通常被称为ok。在稍后我们将看到的三个操作都是类似的用法。如果map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功:

1
2
3
v, ok = m[key]             // map lookup
v, ok = x.(T) // type assertion
v, ok = <-ch // channel receive

译注:map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于只产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发生运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。例如下面的例子:

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

_, ok = m[key] // map返回2个值
_, ok = mm[""], false // map返回1个值
_ = mm[""] // map返回1个值

和变量声明一样,我们可以用下划线空白标识符_来丢弃不需要的值。

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

类型

为了说明类型声明,我们将不同温度单位分别定义为不同的类型:

gopl.io/ch2/tempconv0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。Celsius(t)和Fahrenheit(t)是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。另一方面,CToF和FToC两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。

对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0))。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。如果x是可以赋值给T类型的值,那么x必然也可以被转为T类型,但是一般没有这个必要。

例子1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>package main
>import (
"fmt"
>)
>type Celsius float64 // 摄氏温度
>type Fahrenheit float64 // 华氏温度
>const (
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温度
BoilingC Celsius = 100 // 沸水温度
>)
>func main() {
c := Celsius(100)
f := Fahrenheit(15)
fmt.Println(c+f)
>}

报错: invalid operation: c + f (mismatched types Celsius and Fahrenheit)

在Go语言中,不能直接将不同类型的自定义类型进行相加,即使它们的底层类型是相同的。虽然 CelsiusFahrenheit 都是基于 float64 类型的自定义类型,但它们被视为完全不同的类型。

这是因为在Go语言中,类型的名称包括了类型本身和其所属的包。在这种情况下,CelsiusFahrenheit 都是包级别的类型,即使它们的底层类型都是 float64,Go语言也不会自动进行类型转换。因此,编译器会报错,提示类型不匹配。

要实现相加操作,我们需要显式地将它们转换为相同的类型。在之前的修正代码中,我们使用了类型转换将 CelsiusFahrenheit 分别转换为 float64 类型,然后再进行相加。最后,我们再将相加后的结果转换为 Celsius 类型。这样做是因为Go语言强制执行类型安全,不会自动进行不同类型的隐式转换。

所以,为了在Go语言中相加不同类型的值,我们需要显式地进行类型转换。这样可以确保类型安全,避免意外的类型错误。

例2:该代码能过编译

1
2
3
4
5
6
7
8
9
10
>#include <stdio.h>
>typedef double Celsius;
>typedef double Fahrenheit;
>int main() {
Celsius c = 100;
Fahrenheit f = 15;

printf("%lf\n", c+f);
return 0;
>}
  • 请注意,C语言并不执行类型检查,所以编译器不会报错,即使相加的类型不同。这可能导致潜在的运行时错误或意外的结果。因此,在C语言中,程序员需要特别小心,并确保进行适当的类型转换以避免错误。

因此正确的形式应为

在C语言中,不同类型的自定义类型不能直接相加,就像Go语言中一样。C语言也要求显式地进行类型转换,以确保类型安全。

在C语言中,我们可以通过显式类型转换将不同类型的值进行相加。例如,假设我们有两个自定义类型 CelsiusFahrenheit,它们都是基于 double 类型的自定义类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cCopy code
typedef double Celsius;
typedef double Fahrenheit;

int main() {
Celsius c = 100;
Fahrenheit f = 15;

// 需要显式类型转换
double sum = (double)c + (double)f;
printf("%lf\n", sum);

return 0;
}

在这个C语言代码中,我们将 CelsiusFahrenheit 都定义为 double 的类型别名。然后,在 main 函数中,我们声明一个 Celsius 类型的变量 c 和一个 Fahrenheit 类型的变量 f。由于它们都是 double 的别名,我们需要在相加前进行显式类型转换。

包和文件

包的初始化

这个算法?

下面的代码定义了一个PopCount函数,用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pc,pc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数,这样的话在处理64bit宽度的数字时就没有必要循环64次,只需要8次查表就可以了。(这并不是最快的统计1bit数目的算法,但是它可以方便演示init函数的用法,并且演示了如何预生成辅助表格,这是编程中常用的技术)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package popcount

// pc[i] is the population count of i.
var pc [256]byte

func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}

// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
return int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}

译注:对于pc这类需要复杂处理的初始化,可以通过将初始化逻辑包装为一个匿名函数处理,像下面这样:

1
2
3
4
5
6
7
// pc[i] is the population count of i.
var pc [256]byte = func() (pc [256]byte) {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
return
}()

要注意的是在init函数中,range循环只使用了索引,省略了没有用到的值部分。循环也可以这样写:

1
for i, _ := range pc {

作用域

作用域:声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。
生命周期:一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

注意:任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。于导入的包,例如tempconv导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包。

slice

  1. 如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大:另外,字符串的切片操作和[]byte字节类型切片的切片操作是类似的。都写作x[m:n],并且都是返回一个原始字节序列的子序列,底层都是共享之前的底层数组,因此这种操作都是常量时间复杂度。x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。

因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名(§2.3.2)。

1
2
3
4
5
6
7
months := [...]string{1: "January", /* ... */, 12: "December"}
summer := months[6:9]
fmt.Println(summer) // ["June" "July" "August"]
fmt.Println(summer[:20]) // panic: out of range

endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer) // "[June July August September October]"

From:ChatGPT:在Go语言中,对切片进行操作时,有两个相关的属性:len()cap()

  • len(s):返回切片s的当前长度,表示切片中实际存储的元素个数。
  • cap(s):返回切片s的容量,表示切片底层数组的大小。
  1. fmt.Println(summer[:20]):在这里,尝试获取summer切片中索引0到19的元素。如果summer切片的长度(len(summer))小于20,那么这个操作将超出切片的长度范围,会导致运行时错误,触发panic异常。这是因为Go语言在切片操作时会检查索引是否在合法范围内,不允许访问超出切片长度的索引。
  2. endlessSummer := summer[:5]:这里创建了一个新的切片endlessSummer,它是从summer切片的索引0开始,一直到索引4(包括索引4)的一个子切片。这个操作并没有超出summer切片的容量(cap(summer)),因为切片容量表示底层数组的大小,而endlessSummer的长度(len(endlessSummer))为5,在summer的容量范围内。因此,这个操作是合法的。

总结:对于切片操作,当索引超出切片的长度(len())时,会导致panic异常。但是,如果切片操作在切片的容量范围内(cap()),可以有效地扩展切片。这种扩展产生一个新的切片,新切片的长度可以大于原始切片的长度。要注意的是,在扩展切片时,不能超出切片的容量,否则也会触发panic异常。