《Go 语言入门经典》学习笔记

251 阅读14分钟

3. 变量

3.5 变量声明方式

所有的变量声明方式:


var s string = "hello world"

var s = "hello world"

var t string

t = "hello world"

U := "hello world"

  1. 不能在函数外面使用简短变量声明。

  2. 约定:在函数内使用简短变量声明,在函数外省略类型。

3.7 使用指针

  1. 在 Go 语言中声明变量时,将在计算机内存中给它分配一个位置,以便能够存储、修改和获取变量的值。要获取变量在计算机内存中的地址,可在变量名前加上 & 字符。

  2. 将变量传递给函数时,会分配新内存并将变量的值复制到其中。这样将有两个变量实例,它们位于不同的内存单元中。一般而言,这不可取,因为这将占用更多的内存,同事由于存在变量的多个副本,很容易引入 Bug。考虑到这一点,Go 提供了指针。

  3. 指针是 Go 语言中的一种类型,指向变量所在的内存单元。要声明指针,可在变量名前加上星号字符。

  4. 如果要使用指针指向的变量的值,而不是其内存地址,可在指针变量前加上星号。

3.8 声明常量

  1. 常量指的是在整个程序生命周期内都不变的值。常量初始化后,可以引用它,但不能修改它。

3.10 问与答

问:在 Go 语言中,有很多声明变量的方式,我怎么知道该使用哪种方式呢?

答:要理解Go语言约定,一种不错的方式是查看 Go 本身的源代码。选择您熟悉的包名,并查看其代码中的变量声明。

问:不声明变量的类型是不是很危险?编译器在定义变量的类型时会不会出现错误呢?

答:对于未显式声明类型的变量,Go 编译器很善于动态地推断其类型。如果编译器推断不出来(这种可能性很小),它会告诉您的。

4. 使用函数

4.2 定义不定参数函数

  1. 不定参数函数是指参数数量不确定的函数、通俗地说,这意味着它们接受可变数量的参数。在 Go 语言中,能够传递可变数量的参数,但它们的类型必须与函数签名指定的类型相同。要指定不定单数,可使用 3 个点(...)。例如:func sumNumbers(numbers...int) int {}

4.3 使用具名返回值

  1. 具名返回值让函数能够在返回前将值赋给具名变量,这有助于提升函数的可读性,使其功能更加明确。要使用具名返回值,可在函数签名的返回值部分指定变量名。例如:func sayHi() (x, y string) {}

4.4 使用递归函数

  1. 递归函数虽然是一个简单的概念,却也是一个功能强大的编程元素。递归函数是不断调用自己直到满足特定条件的函数。要在函数中实现递归,可将调用自己的代码作为终止语句中的返回值。

4.7 问与答

问:声明函数后,可修改其参数的数量和类型吗?

答:不能。函数声明后,编译器就记录了其签名,并根据签名检查调用该函数时指定的参数数量和类型是否正确。

问:该使用具名返回值吗?

答:在简短的函数中,可使用具名返回值,但这样可能导致代码阅读起来更困难;在有些情况下,还更容易引入 Bug。编写更啰唆的代码没什么不好,但仅当有助于改善可维护性和可读性时才如此。

问:为何应让函数较短?

答:简短的函数有多个优点。它们更易于理解和测试,还让函数能够专注于做一件事情并把它做好。使用单个庞大的函数可能导致代码成为联系紧密的整体,进而难以修改。

4.8 总结

  1. 在 Go 语言中,函数可返回一个或多个值。另外,多个返回值的类型可以不同。

  2. 调用自己的函数被称为递归函数。

  3. 在 Go 语言中,可将函数作为参数传递给其他函数。函数被视为一种类型,因此可以传递。在 Go 语言中,函数是一等公民,因为可将其作为参数传递给其他函数。

5. 控制流程

5.9 使用 defer 语句

  1. defer 是一个很有用的 Go 语言功能,它能够让您在函数返回前执行另一个函数。

5.10 问答

问:为何要使用 defer 语句,而不直接使用常规的控制流程?

答:一些需要使用 defer 语句的例子包括:在读取文件后将其关闭、收到来自 Web 服务器的响应后对其进行处理以及建立连接后向数据库请求数据。需要在某项工作完成后执行特定的函数时,defer 语句是不错的选择。

问:有多条 defer 语句时,函数返回前将在按什么样的顺序执行它们?

答:多条 defer 语句按相反的顺序执行。这意味着离函数末尾最近的 defer 语句最先执行。

6. 数组、切片和映射

6.5 问答

问:该使用数组还是切片?

答:除非确定必须使用数组,否则请使用切片。切片能够让您轻松地添加和删除元素,还无须处理内存分配问题。

问:没有从切片中删除元素的内置函数吗?

答:不能将 delete 用于切片。没有专门用于从切片中删除元素的函数,但可使用内置函数 append 来完成这种任务,您还可创建子切片。

问:需要指定映射的长度吗?

答:不需要。使用内置函数 make 创建映射时,可使用第二个参数,但这个参数只是容量提示,而非硬性规定。映射可根据要存储的元素数量自动增大,因此没有必要指定长度。

7. 使用结构体和指针

7.7 区分指针引用和值引用

  1. 数据值存储在计算机内存中。指针包含值得内存地址,这意味着使用指针可读写存储的值。创建结构体实例时,给数据字段分配内存并给它们指定默认值;然后返回指向内存的指针,并将其赋给一个变量。使用简短变量赋值时,将分配内存并指定默认值。

a := Drink{}

  1. 复制结构体时,明确内存方面的差别很重要。将指向结构体的变量赋给另一个时,被称为赋值。赋值后,a 与 b 相同,但它是 b 的副本,而不是指向 b 的引用。修改 b 不会影响 a,反之亦然。

a := b

  1. 要修改原始结构体实例包含的值,必须使用指针。指针是指向内存地址的引用,因此使用它操作的不是结构体的副本而是其本身,要获得指针,可在变量名前加上 & 符号。

type Drink struct {

    Name string

    Ice bool

}

a := Drink{

    Name: "Lemonade",

    Ice: true,

}

b := &a

  1. 要创建结构体的副本,但不希望修改影响原始结构体时,应使用值;要操作结构体的副本,并希望所做的修改在原始结构体中反映出来时,应使用指针。

8. 创建方法和接口

8.1 使用方法

  1. 方法类似于函数,但有一点不同:在关键字 func 后面添加了另一个参数部分,用于接收单个参数。例如:

type Movie struct {

    Name string

    Rating float32

}

func (m *Movie) summary() string {

    // code

}

  1. 请注意,在方法声明中,关键字 func 后面多了一个参数——接受者。严格地说,方法接收者是一种类型,这里是指向结构体 Movie 的指针。接下来是方法名、参数以及返回类型。除多了包含接收者的参数部分外,方法于函数完全相同。可将接收者视为与方法相关联的东西。通过声明方法 summary,让结构体 Movie 的任何实例都可使用它。

  2. 下面的函数与前面的方法声明等价。


type Movie struct {

    Name string

    Rating float32

}

func summary(m *Movie) string {

    // code

}

  1. 函数 summary 和结构体 Movie 相互依赖,但它们之间没有直接关系。例如,如果不能访问结构体 Movie 的定义,就无法声明函数 summary。如果使用函数,则在每个使用函数或结构体的地方,都需包含函数和结构体的定义,这会导致代码重复。另外,函数发生任何改变,都必须随之修改多个地方。这样看来在函数与结构体关系密切时,使用方法更合理。

  2. 使用方法的优点在于,只需编写方法实现一次,就可对结构体的任何实例进行调用。

8.6 问答

问:函数和方法有何不同?

答:严格地说,方法和函数的唯一差别在于,方法多了一个指定接收者的参数,这让您能够对数据类型调用方法,从而提高代码重用性和模块化程度。

问:在什么情况下应使用指针引用?在什么情况下应使用值引用?

答:如果需要修改原始结构体中的数据,就使用指针;如果要操作原始数据的副本,就使用值引用。

问:接口的实现可包含接口中没有的方法吗?

答:可以。可在接口的实现中添加额外的方法,但这仅适用于结构体,而不适用于接口。

10. 处理错误

10.1 错误处理及 Go 语言的独特之处

  1. 在 Go 语言中,有一种约定是,如果没有发生错误,返回的错误值将为 nil。

10.7 慎用 panic

  1. panic 是 Go 语言的一个内置函数,它终止正常的控制流程并引发恐慌,导致程序停止执行。出现普通错误时,并不提倡这种做法,因为程序将停止执行,并且没有任何回旋余地。

11. 使用 Goroutine

11.5 使用 Goroutine 处理并发操作

  1. Goroutine 使用起来非常简单,只需要让 Goroutine 执行的函数或方法前加上关键字 go 即可。

12. 通道简介

12.1 使用通道

  1. 不要通过共享内存来通信,而通过通信来共享内存。

  2. 通道使用示例:


messages := make(chan string) // 创建缓冲通道

c <- "Hello World" // 向通道发送消息

msg := <-c // 从通道接收消息

close(messages) // 关闭通道

12.4 将通道用作函数参数

  1. 通道可以作为参数传递给函数,并在函数中向通道发送消息。要进一步指定在函数中如何使用传入的通道,可在传递通道时将其指定为只读、只写或读写的。

  2. <- 位于关键字 chan 左边时,表示通道在函数内是只读的,例如:func channelReader (messages <-chan string) {

  3. <- 位于关键字 chan 右边时,表示通道在函数内是只写的,例如:func channelWriter (messages chan<- string) {

  4. 没有指定 <- 时,表示通道时可读写的,例如:func channelReaderAndWriter (messages chan string) {

12.5 使用 select 语句

  1. 假设有多个 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)

}

  1. 具体执行哪条 case 语句,取决于消息到达的时间,哪条消息最先到达决定了将执行哪条 case 语句。通常,接下来收到的其他消息将被丢弃。收到一条消息后,select 语句将不再阻塞。

  2. 如果没有接收到消息,可以设置超时时间。这让 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 退出通道

  1. 程序需要使用 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 导入包

  1. main 包是一种特殊的包,其特殊之处在于不能导入。对 main 包的唯一要求是,必须声明一个 main 函数,这个函数不接受任何参数且不返回任何值。简而言之,main 包是程序的入口。

15. 测试和性能

15.1 测试:软件开发最重要的方面

  1. 常见的测试有多种:

    1. 单元测试

    2. 功能测试

    3. 集成测试

15.1.1 单元测试

  1. 单元测试针对一小部分代码,并独立地对他们进行测试。

15.1.2 集成测试

  1. 集成测试通常测试的是应用程序各部分协同工作的情况。

15.1.3 功能测试

  1. 功能测试通常被称为端到端测试或由外向内的测试。

15.2 testing 包

  1. 约定:

  2. Go 测试与其测试的代码在一起。测试不是放在独立的测试目录中,而是与它们要测试的代码放在同一个目录中。测试文件是这样命名的:在要测试的文件的名称后面加上后缀 _test

  3. 测试为名称以单词 Test 打头的函数。

  4. 在测试包中创建两个变量:got 和 want,它们分别表示要测试的值以及期望的值。

20. 处理 JSON

  1. Json 使用:

jsonByteData, err := json.Marshall(p) // 结构体转 JSON

err := json.Unmarshal(jsonByteData, &p) // 解码

21. 处理文件

21.2 使用 ioutil 包读写文件

21.2.1 读取文件

  1. 读取文件

fileBytes, err := ioutil.ReadFile("tmp.txt")

fileString := string(fileBytes)

21.2.2 创建文件

  1. 使用 WriteFile 函数来讲数据写入文件,也可以使用它来创建文件。函数 WriteFile 接受一个文件名、要写入的文件的数据以及应用于文件的权限。

  2. 文件权限是从 UNIX 权限衍生而来的,它们对应于3类用户:文件所有者、与文件位于同一组的用户、其他用户。

  3. 创建文件示例:


b := make([]byte, 0)

err := ioutil.WriteFile("tmp2.txt", b, 06)

21.3 写入文件

  1. 写入文件

s := "hello world"

err := ioutil.WriteFile("tmp2.txt", []byte(s), 0644)

21.4 列出目录的内容

  1. 要处理文件系统中的文件,必须知道目录结构。为此,ioutil 包提供了便利函数 ReadDir,它接受以字符串方式指定的目录名,并返回一个列表,其中包含按文件名排序的文件。文件名的类型为 FileInfo,包含如下信息:

    1. Name:文件的名称

    2. Size:文件的长度,单位为字节

    3. Mode:用二进制位表示的权限

    4. ModTime:文件最后一个被修改的时间

    5. IsDir:文件是否是目录

    6. Sys:底层数据源

22. 正则表达式

  1. 使用 regexp

23. Go 语言时间编程

23.4 使用ticker

  1. 使用 ticker 可让代码每隔特定的时间就重复执行一次。需要在很长的时间内定期执行任务时,这么做很有用。

c := time.Tick(5 * time.Second)

for t := range c {

    fmt.Printf("The time is now %v\n", t)

}

24. 部署 Go 语言代码

24.1 理解目标

  1. 使用 go env 来获悉有关操作系统和体系结构的详细信息。