3. 变量
3.5 变量声明方式
所有的变量声明方式:
var s string = "hello world"
var s = "hello world"
var t string
t = "hello world"
U := "hello world"
-
不能在函数外面使用简短变量声明。
-
约定:在函数内使用简短变量声明,在函数外省略类型。
3.7 使用指针
-
在 Go 语言中声明变量时,将在计算机内存中给它分配一个位置,以便能够存储、修改和获取变量的值。要获取变量在计算机内存中的地址,可在变量名前加上
&字符。 -
将变量传递给函数时,会分配新内存并将变量的值复制到其中。这样将有两个变量实例,它们位于不同的内存单元中。一般而言,这不可取,因为这将占用更多的内存,同事由于存在变量的多个副本,很容易引入 Bug。考虑到这一点,Go 提供了指针。
-
指针是 Go 语言中的一种类型,指向变量所在的内存单元。要声明指针,可在变量名前加上星号字符。
-
如果要使用指针指向的变量的值,而不是其内存地址,可在指针变量前加上星号。
3.8 声明常量
- 常量指的是在整个程序生命周期内都不变的值。常量初始化后,可以引用它,但不能修改它。
3.10 问与答
问:在 Go 语言中,有很多声明变量的方式,我怎么知道该使用哪种方式呢?
答:要理解Go语言约定,一种不错的方式是查看 Go 本身的源代码。选择您熟悉的包名,并查看其代码中的变量声明。
问:不声明变量的类型是不是很危险?编译器在定义变量的类型时会不会出现错误呢?
答:对于未显式声明类型的变量,Go 编译器很善于动态地推断其类型。如果编译器推断不出来(这种可能性很小),它会告诉您的。
4. 使用函数
4.2 定义不定参数函数
- 不定参数函数是指参数数量不确定的函数、通俗地说,这意味着它们接受可变数量的参数。在 Go 语言中,能够传递可变数量的参数,但它们的类型必须与函数签名指定的类型相同。要指定不定单数,可使用 3 个点(...)。例如:
func sumNumbers(numbers...int) int {}
4.3 使用具名返回值
- 具名返回值让函数能够在返回前将值赋给具名变量,这有助于提升函数的可读性,使其功能更加明确。要使用具名返回值,可在函数签名的返回值部分指定变量名。例如:
func sayHi() (x, y string) {}
4.4 使用递归函数
- 递归函数虽然是一个简单的概念,却也是一个功能强大的编程元素。递归函数是不断调用自己直到满足特定条件的函数。要在函数中实现递归,可将调用自己的代码作为终止语句中的返回值。
4.7 问与答
问:声明函数后,可修改其参数的数量和类型吗?
答:不能。函数声明后,编译器就记录了其签名,并根据签名检查调用该函数时指定的参数数量和类型是否正确。
问:该使用具名返回值吗?
答:在简短的函数中,可使用具名返回值,但这样可能导致代码阅读起来更困难;在有些情况下,还更容易引入 Bug。编写更啰唆的代码没什么不好,但仅当有助于改善可维护性和可读性时才如此。
问:为何应让函数较短?
答:简短的函数有多个优点。它们更易于理解和测试,还让函数能够专注于做一件事情并把它做好。使用单个庞大的函数可能导致代码成为联系紧密的整体,进而难以修改。
4.8 总结
-
在 Go 语言中,函数可返回一个或多个值。另外,多个返回值的类型可以不同。
-
调用自己的函数被称为递归函数。
-
在 Go 语言中,可将函数作为参数传递给其他函数。函数被视为一种类型,因此可以传递。在 Go 语言中,函数是一等公民,因为可将其作为参数传递给其他函数。
5. 控制流程
5.9 使用 defer 语句
- defer 是一个很有用的 Go 语言功能,它能够让您在函数返回前执行另一个函数。
5.10 问答
问:为何要使用 defer 语句,而不直接使用常规的控制流程?
答:一些需要使用 defer 语句的例子包括:在读取文件后将其关闭、收到来自 Web 服务器的响应后对其进行处理以及建立连接后向数据库请求数据。需要在某项工作完成后执行特定的函数时,defer 语句是不错的选择。
问:有多条 defer 语句时,函数返回前将在按什么样的顺序执行它们?
答:多条 defer 语句按相反的顺序执行。这意味着离函数末尾最近的 defer 语句最先执行。
6. 数组、切片和映射
6.5 问答
问:该使用数组还是切片?
答:除非确定必须使用数组,否则请使用切片。切片能够让您轻松地添加和删除元素,还无须处理内存分配问题。
问:没有从切片中删除元素的内置函数吗?
答:不能将 delete 用于切片。没有专门用于从切片中删除元素的函数,但可使用内置函数 append 来完成这种任务,您还可创建子切片。
问:需要指定映射的长度吗?
答:不需要。使用内置函数 make 创建映射时,可使用第二个参数,但这个参数只是容量提示,而非硬性规定。映射可根据要存储的元素数量自动增大,因此没有必要指定长度。
7. 使用结构体和指针
7.7 区分指针引用和值引用
- 数据值存储在计算机内存中。指针包含值得内存地址,这意味着使用指针可读写存储的值。创建结构体实例时,给数据字段分配内存并给它们指定默认值;然后返回指向内存的指针,并将其赋给一个变量。使用简短变量赋值时,将分配内存并指定默认值。
a := Drink{}
- 复制结构体时,明确内存方面的差别很重要。将指向结构体的变量赋给另一个时,被称为赋值。赋值后,a 与 b 相同,但它是 b 的副本,而不是指向 b 的引用。修改 b 不会影响 a,反之亦然。
a := b
- 要修改原始结构体实例包含的值,必须使用指针。指针是指向内存地址的引用,因此使用它操作的不是结构体的副本而是其本身,要获得指针,可在变量名前加上
&符号。
type Drink struct {
Name string
Ice bool
}
a := Drink{
Name: "Lemonade",
Ice: true,
}
b := &a
- 要创建结构体的副本,但不希望修改影响原始结构体时,应使用值;要操作结构体的副本,并希望所做的修改在原始结构体中反映出来时,应使用指针。
8. 创建方法和接口
8.1 使用方法
- 方法类似于函数,但有一点不同:在关键字
func后面添加了另一个参数部分,用于接收单个参数。例如:
type Movie struct {
Name string
Rating float32
}
func (m *Movie) summary() string {
// code
}
-
请注意,在方法声明中,关键字 func 后面多了一个参数——接受者。严格地说,方法接收者是一种类型,这里是指向结构体 Movie 的指针。接下来是方法名、参数以及返回类型。除多了包含接收者的参数部分外,方法于函数完全相同。可将接收者视为与方法相关联的东西。通过声明方法 summary,让结构体 Movie 的任何实例都可使用它。
-
下面的函数与前面的方法声明等价。
type Movie struct {
Name string
Rating float32
}
func summary(m *Movie) string {
// code
}
-
函数 summary 和结构体 Movie 相互依赖,但它们之间没有直接关系。例如,如果不能访问结构体 Movie 的定义,就无法声明函数 summary。如果使用函数,则在每个使用函数或结构体的地方,都需包含函数和结构体的定义,这会导致代码重复。另外,函数发生任何改变,都必须随之修改多个地方。这样看来在函数与结构体关系密切时,使用方法更合理。
-
使用方法的优点在于,只需编写方法实现一次,就可对结构体的任何实例进行调用。
8.6 问答
问:函数和方法有何不同?
答:严格地说,方法和函数的唯一差别在于,方法多了一个指定接收者的参数,这让您能够对数据类型调用方法,从而提高代码重用性和模块化程度。
问:在什么情况下应使用指针引用?在什么情况下应使用值引用?
答:如果需要修改原始结构体中的数据,就使用指针;如果要操作原始数据的副本,就使用值引用。
问:接口的实现可包含接口中没有的方法吗?
答:可以。可在接口的实现中添加额外的方法,但这仅适用于结构体,而不适用于接口。
10. 处理错误
10.1 错误处理及 Go 语言的独特之处
- 在 Go 语言中,有一种约定是,如果没有发生错误,返回的错误值将为 nil。
10.7 慎用 panic
- panic 是 Go 语言的一个内置函数,它终止正常的控制流程并引发恐慌,导致程序停止执行。出现普通错误时,并不提倡这种做法,因为程序将停止执行,并且没有任何回旋余地。
11. 使用 Goroutine
11.5 使用 Goroutine 处理并发操作
- Goroutine 使用起来非常简单,只需要让 Goroutine 执行的函数或方法前加上关键字
go即可。
12. 通道简介
12.1 使用通道
-
不要通过共享内存来通信,而通过通信来共享内存。
-
通道使用示例:
messages := make(chan string) // 创建缓冲通道
c <- "Hello World" // 向通道发送消息
msg := <-c // 从通道接收消息
close(messages) // 关闭通道
12.4 将通道用作函数参数
-
通道可以作为参数传递给函数,并在函数中向通道发送消息。要进一步指定在函数中如何使用传入的通道,可在传递通道时将其指定为只读、只写或读写的。
-
<- 位于关键字 chan 左边时,表示通道在函数内是只读的,例如:
func channelReader (messages <-chan string) { -
<- 位于关键字 chan 右边时,表示通道在函数内是只写的,例如:
func channelWriter (messages chan<- string) { -
没有指定 <- 时,表示通道时可读写的,例如:
func channelReaderAndWriter (messages chan string) {
12.5 使用 select 语句
- 假设有多个 Goroutine,而程序将根据最先返回的 Goroutine 执行相应的操作,此时可使用 select 语句。
channel1 := make(chan string)
channel2 := make(chan string)
select {
case msg1 := <-channel1:
fmt.Println("received", msg1)
case msg2 := <-channel2:
fmt.Println("received", msg2)
}
-
具体执行哪条 case 语句,取决于消息到达的时间,哪条消息最先到达决定了将执行哪条 case 语句。通常,接下来收到的其他消息将被丢弃。收到一条消息后,select 语句将不再阻塞。
-
如果没有接收到消息,可以设置超时时间。这让 select 语句在指定时间后不再阻塞,以便接着往下执行。
channel1 := make(chan string)
channel2 := make(chan string)
select {
case msg1 := <-channel1:
fmt.Println("received", msg1)
case msg2 := <-channel2:
fmt.Println("received", msg2)
case <- time.After(500 * time.Millisecond):
fmt.Println("no messages received.giving up.")
}
12.6 退出通道
- 程序需要使用 select 语句实现无限制地阻塞,但同时要求能够随时返回。通过在 select 语句中添加一个退出通道,可向退出通道发送消息来结束该语句,从而停止阻塞。可将退出通道视为阻塞式 select 语句的开关。对于退出通道,可随便命名,但通常将其命名为 stop 或 quit。
messages := make(chan string)
stop := make(chan bool)
for {
select {
case <-stop:
return
case msg := <-messages:
fmt.Println(msg)
}
}
12.8 问答
问:可给通道执行多种数据类型吗?
答:不能。通道只能有一种数据类型。您可创建任何类型的通道,因此可使用结构体来存储复杂的数据结构。
问:在 select 语句中,如果同时从两个通道那里收到消息,结果将如何?
答:将随机地选择并执行一条 case 语句,且只执行被选中的 case 语句。
问:关闭通道时会导致缓冲的消息丢失吗?
答:关闭缓冲通道意味着不能再向它发送消息。缓冲的消息会被保留,可供接收者读取。
问:相比于 Goroutine,通道有何优点?
答:通道与 Goroutine 互为补充,它让 Goroutine 能够相互通信。这给程序员提供了一个受控的并发编程环境。
问:select 语句中的超时时间有何用途?
答:通过使用超时时间,可在指定时间过后从 select 语句返回,从而结束阻塞操作。select 语句根据最先到达的消息执行相应的 case 语句;通过指定超时时间,可在给定时间内没有收到任何消息时从 select 语句返回。
13. 使用包实现代码重用
13.1 导入包
- main 包是一种特殊的包,其特殊之处在于不能导入。对 main 包的唯一要求是,必须声明一个 main 函数,这个函数不接受任何参数且不返回任何值。简而言之,main 包是程序的入口。
15. 测试和性能
15.1 测试:软件开发最重要的方面
-
常见的测试有多种:
-
单元测试
-
功能测试
-
集成测试
-
15.1.1 单元测试
- 单元测试针对一小部分代码,并独立地对他们进行测试。
15.1.2 集成测试
- 集成测试通常测试的是应用程序各部分协同工作的情况。
15.1.3 功能测试
- 功能测试通常被称为端到端测试或由外向内的测试。
15.2 testing 包
-
约定:
-
Go 测试与其测试的代码在一起。测试不是放在独立的测试目录中,而是与它们要测试的代码放在同一个目录中。测试文件是这样命名的:在要测试的文件的名称后面加上后缀
_test。 -
测试为名称以单词 Test 打头的函数。
-
在测试包中创建两个变量:got 和 want,它们分别表示要测试的值以及期望的值。
20. 处理 JSON
- Json 使用:
jsonByteData, err := json.Marshall(p) // 结构体转 JSON
err := json.Unmarshal(jsonByteData, &p) // 解码
21. 处理文件
21.2 使用 ioutil 包读写文件
21.2.1 读取文件
- 读取文件
fileBytes, err := ioutil.ReadFile("tmp.txt")
fileString := string(fileBytes)
21.2.2 创建文件
-
使用
WriteFile函数来讲数据写入文件,也可以使用它来创建文件。函数WriteFile接受一个文件名、要写入的文件的数据以及应用于文件的权限。 -
文件权限是从 UNIX 权限衍生而来的,它们对应于3类用户:文件所有者、与文件位于同一组的用户、其他用户。
-
创建文件示例:
b := make([]byte, 0)
err := ioutil.WriteFile("tmp2.txt", b, 06)
21.3 写入文件
- 写入文件
s := "hello world"
err := ioutil.WriteFile("tmp2.txt", []byte(s), 0644)
21.4 列出目录的内容
-
要处理文件系统中的文件,必须知道目录结构。为此,ioutil 包提供了便利函数 ReadDir,它接受以字符串方式指定的目录名,并返回一个列表,其中包含按文件名排序的文件。文件名的类型为 FileInfo,包含如下信息:
-
Name:文件的名称
-
Size:文件的长度,单位为字节
-
Mode:用二进制位表示的权限
-
ModTime:文件最后一个被修改的时间
-
IsDir:文件是否是目录
-
Sys:底层数据源
-
22. 正则表达式
- 使用
regexp包
23. Go 语言时间编程
23.4 使用ticker
- 使用
ticker可让代码每隔特定的时间就重复执行一次。需要在很长的时间内定期执行任务时,这么做很有用。
c := time.Tick(5 * time.Second)
for t := range c {
fmt.Printf("The time is now %v\n", t)
}
24. 部署 Go 语言代码
24.1 理解目标
- 使用
go env来获悉有关操作系统和体系结构的详细信息。