《Go语言圣经》读书笔记

271 阅读9分钟

1.1. Hello, World

-go是编译型语言。go有一系列命令,如go run编译一个或多个以.go结尾的源文件并链接库文件,生成可执行文件(二进制文件)并执行

-go的代码通过包(package)组织;其中,main包比较特殊,定义了一个独立可执行的程序,而不是一个库;main里的main 函数是整个程序执行时的入口

1.2. 命令行参数

-程序的命令行参数可从os包的Args变量获取。os.Args的第一个元素os.Args[0],是命令本身的名字;其它的元素则是程序启动时传给它的参数

2.1. 命名

-命名规则:必须以一个字母或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的

-局部变量与全局变量:变量名在函数内部定义,那么它就只在函数内部有效;变量名在函数外部定义,那么将在当前包的所有文件中都可以访问

包导出变量与包私有变量:名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(必须是在函数外部定义的),那么它将可以被外部的包访问;如果名字是小写字母开头的,那么将只能被当前包的文件访问

包本身的名字一般总是用小写字母

2.2. 声明

四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明

2.3. 变量

-变量声明的一般语法:var 变量名字 类型 = 表达式

如果省略类型信息,那么将根据初始化表达式来推导变量的类型信息

如果初始化表达式被省略,将用零值初始化,因此在Go语言中不存在未初始化的变量

-不同类型变量的零值

数值类型变量对应的零值是0,布尔类型变量对应的零值是false

字符串类型对应的零值是空字符串“”

接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil

数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值

-简短变量声明语句:“名字 := 表达式”,变量的类型根据表达式来自动推导

简短变量声明被广泛用于大部分的局部变量的声明和初始化

-简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了

第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量,然后对已经声明的err进行了赋值操作。

 in, err := os.Open(infile) 

// ... 

out, err := os.Create(outfile) 

简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过: 

 f, err := os.Open(infile) 

// ... 

f, err := os.Create(outfile)

-指针基本概念

一个指针的值是另一个变量的地址。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字

var x int声明一个x变量;p:=&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针指针的数据类型是*int;*****p表达式读取指针指向的变量的值,*p=2表示更新指针所指向的变量的值

-任何类型的指针的零值都是nil;指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等

-指针函数调用中的作用:

返回函数中局部变量的地址也是安全的。例如调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效

var p = f()

func f() *int { v := 1 return &v }

每次调用f函数都将返回不同的结果: fmt.Println(f() == f()) // "false"

指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值

-变量生命周期

包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的;

局部变量的生命周期从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建

-垃圾回收原理:

每个包级的变量每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历是否可以找到该变量,不可达的则可以回收。所以,循环迭代内部的局部变量的生命周期可能超出其局部作用域,局部变量可能在函数返回之后依然存在

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

逃逸

 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的存储空间(译注:也可能选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间)

虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收,从而可能影响程序的性能

2.4赋值

只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的

对于两个值是否可以用==!=进行相等比较的能力也和可赋值性有关系

可赋值性的规则对于不同类型有着不同要求

2.5. 类型

变量或表达式的类型定义了对应存储值的属性特征,例如数值在内存的存储大小

-类型转换

type Celsius float64 // 摄氏温度

type Fahrenheit float64 // 华氏温度

var c Celsius

var f Fahrenheit

fmt.Println(c == f) // compile error: type mismatch

Celsius和Fahrenheit虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。

Celsius(t)或Fahrenheit(t)显式类型转化操作才能将float64转为对应的类型。它们并不是函数调用,类型转换不会改变值本身,但是会使它们的语义发生变化

对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型。如果T是指针类型,可能会需要用小括弧包装T,比如**(*int)(0)。只有当两个类型的底层基础类型相同**时,才允许这种转型操作

数值类型之间的转型也是允许的,并且在字符串和一些特定类型的slice之间也是可以转换的。这类转换只改变值的表现,并不改变值本身。例如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为[]byte的slice将拷贝一个字符串数据的副本。在任何情况下,运行时不会发生转换失败的错误

-类型的方法集

命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集

func (c Celsius) String() string {

 return fmt.Sprintf("%g°C", c) 

}

 许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印

2.6. 包和文件

每个包都对应一个独立的名字空间

包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息:如果一个名字是大写字母开头的,那么该名字是导出的

-包的导入路径、导入声明和包名

每个包都有一个全局唯一的导入路径。import 语句+导入路径即为导入声明

通常一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录**路径是$GOPATH/src/**gopl.io/ch1/helloworld

按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。导入声明将允许我们以tempconv.CToF的形式来访问包中的内容,但是我们也可以绑定到另一个名称,以避免名字冲突

如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理

-包的初始化

1)包变量的初始化:按照包级变量声明出现的顺序依次初始化包

var a = b + c // a 第三个初始化, 为 3 

var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)

 var c = 1 // c 第一个初始化, 为 1 

 func f() int { return c + 1 }

如果包中含有多个.go源文件,它们将按文件名顺序进行初始化

2)init函数

每个文件都可以包含多个init初始化函数 func init() { /* ... */ }

在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用

3)每个包只会被初始化一次(init只执行一次)

如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了

4)初始化工作是自下而上进行的,main包最后被初始化,以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了

2.7. 作用域