golang圣经第二章:程序结构

描述Go语言程序的基本元素结构、变量、新类型定义、包和文件、以及作用域等概念

本章介绍Go语言程序的命名,声明,基本元素结构、变量、新类型定义、包和文件、以及作用域

2.1 命名

变量名称有大小写区分

关键字

break      default       func     interface   select
case       defer         go       map         struct
chan       else          goto     package     switch
const      fallthrough   if       range       type
continue   for           import   return      var

预定义的名字

内建常量: true false iota nil

内建类型: int int8 int16 int32 int64
          uint uint8 uint16 uint32 uint64 uintptr
          float32 float64 complex128 complex64
          bool byte rune string error

内建函数: make len cap new append copy close delete
          complex real imag
          panic recover

定义域

如果一个名字是在函数内部定义,那么它就只在函数内部有效。

如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。

名字的开头字母的大小写决定了名字在包外的可见性

如果一个名字是大写字母开头的(必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。

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

命名

命名一般采用大小写分隔而不是下划线分隔

QuoteRuneToASCII和parseRequestLine

缩略词一般大小写一致,如htmlEscape、HTMLEscape或escapeHTML,但不会是escapeHtml。

2.2 声明

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

函数声明

一个函数的声明由一个函数名字、参数列表(由函数的调用者提供参数变量的具体值)、一个可选的返回值列表和包含函数定义的函数体组成。

如果函数没有返回值,那么返回值列表是省略的。

2.3 变量

基本声明

变量声明

var 变量名字 类型 = 表达式

其中“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。

声明多个

函数返回值也可以初始化

var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
var f, err = os.Open(name) // os.Open returns a file and an error

不要混淆多个变量的声明和元组的多重赋值(§2.4.1),后者是将右边各个表达式的值赋值给左边对应位置的各个变量:

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

2.3.1 简短变量声明

i := 100                  // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point

简短变量声明语句也可以用来声明和初始化一组变量:

i, j := 0, 1

“:=”是一个变量声明语句,而“=”是一个变量赋值操作

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

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

in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)

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

f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables

简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。

在Go语言中,:== 是两种不同的变量声明和赋值操作符,它们的用法和含义有一些不同。

  1. result := add(3, 5)

这是一个使用短变量声明操作符 := 的语句。它的主要特点是:

  • 它用于声明并初始化一个新的变量 result
  • 变量的类型是由右侧的表达式的返回值类型来推断的,因此 result 的类型将由 add(3, 5) 的返回值类型决定。
  • 这个操作符通常在函数内部使用,用于创建一个新的变量并为其赋初值。
  1. var result = add(3, 5)

这是使用 var 关键字进行变量声明和初始化的语句。它的特点是:

  • 它显式指定了变量的类型,类型由 var result 的部分决定,因此需要明确指定变量的类型。
  • 右侧的表达式 add(3, 5) 用于初始化变量 result 的值。

因此,主要的区别在于类型的推断和声明方式:

  • result := add(3, 5) 会自动推断 result 的类型,不需要显式指定。
  • var result = add(3, 5) 显式指定了变量的类型,适用于需要明确指定类型的情况。

在实际使用中,通常会根据情况选择适当的方式。如果你希望变量的类型由赋值表达式自动推断,可以使用 :=,而如果你需要明确指定类型,可以使用 var

2.3.2 指针

函数可以传指针,不能传引用

任何类型的指针的零值都是nil。

变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受&取地址操作。

指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。

在Go语言中,返回函数中局部变量的地址也是安全的,如下,因为指针p依然引用这个变量。

var p = f()

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

import (
	"flag"
	"fmt"
	"strings"
)

var n = flag.Bool("n", false, "omit newline")
var sep = flag.String("s", " ", "separator")

func main() {
	flag.Parse()
	fmt.Print(strings.Join(flag.Args(), *sep))
	if !*n {
		fmt.Println()
	}
}

2.3.3 new函数

语法糖,不是新的基础概念

调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T

每次调用new函数都是返回一个新的变量的地址

p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)

func newInt() *int {
    return new(int)
}

func newInt() *int {
    var dummy int
    return &dummy
}

重定义

由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型。例如下面的例子:

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

由于new被定义为int类型的变量名,因此在delta函数内部是无法使用内置的new函数的。

2.3.4 变量的生命周期

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

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

函数的参数变量返回值变量都是局部变量。它们在函数每次被调用的时候创建。

末尾的参数变量后面显式插入逗号时,最后插入的逗号不会导致编译错误,这是Go编译器的一个特性

垃圾回收机制

基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。

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

2.3.5 栈和堆的分配

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用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并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。

其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

2.4 赋值

2.4.1 递增,递减

数值变量也可以支持++递增和--递减语句,但是自增和自减是语句,而不是表达式,因此x = i++之类的表达式是错误的

2.4.2 元组赋值

元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。

// 计算两个整数值的的最大公约数
func gcd(x, y int) int {
    for y != 0 {
        x, y = y, x%y
    }
    return x
}
// 计算斐波纳契数列(Fibonacci)的第N个数
func fib(n int) int {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        x, y = y, x+y
    }
    return x
}

有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右边一致。

f, err = os.Open("foo.txt") // function call returns two values

如果map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功:

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

也可能只产生一个结果。对于只产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发生运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)

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)              // 只检测类型,忽略具体值

2.4.3 可赋值性

不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。

目前规则:类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量,常量则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。

对于两个值是否可以用==!=进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。

2.5 类型

type 类型名字 底层类型

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

2.5.1 类型转换

在任何情况下,运行时不会发生转换失败的错误,错误只会发生在编译阶段

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

var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!

2.6 包和文件

每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。

包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。

2.6.1 包注释

在每个源文件的包声明前紧跟着的注释是包注释(§10.7.4)。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释(如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释)。如果包注释很大,通常会放到一个独立的doc.go文件中。

2.6.2 导入包

除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同

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

这种强制规则可以有效减少不必要的依赖,虽然在调试期间可能会让人讨厌,因为删除一个类似log.Print(“got here!”)的打印语句可能导致需要同时删除log包导入声明,否则,编译器将会发出一个错误。在这种情况下,我们需要将不必要的导入删除或注释掉

2.6.3 包的初始化

包的初始化是通过 init() 函数来实现的。每个包可以包含一个或多个 init() 函数,这些函数会在程序启动时自动执行,无需显式调用。

初始化函数的格式如下:

func init() {
    // 初始化代码
}

包中的 init() 函数会在包被导入时执行,每个包的 init() 函数按照导入的顺序执行,但每个 init() 函数仅执行一次。这使得包可以进行一些初始化工作,如设置全局变量、连接数据库、或执行其他必要的操作。

初始化函数对于包的用户是透明的,它们不需要显式调用,而是在包被导入时自动执行。

2.7 作用域

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。

声明语句的作用域是指源代码中可以有效使用这个名字的范围

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

2.7.1 作用域导致的隐晦的错误

var cwd string

func init() {
    cwd, err := os.Getwd() // NOTE: wrong!
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
    log.Printf("Working directory = %s", cwd)
}

上述代码中,虽然cwd在外部已经声明过,但是:=语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。

最直接的解决方法是通过单独声明err变量,来避免使用:=的简短声明方式:

var cwd string

func init() {
    var err error
    cwd, err = os.Getwd()
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

© 2023 github-lanyily. All rights reserved.

Powered by Hydejack v9.1.6