这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
2.1. 命名
Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的:heapSort和Heapsort是两个不同的名字。
Go语言中类似if和switch的关键字有25个;关键字不能用于自定义名字,只能在特定语法结构中使用。
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
此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。
内建常量: 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包外部访问。包本身的名字一般总是用小写字母。
名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;你会经常看到i之类的短名字,而不是冗长的theLoopIndex命名。通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。
在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。因此,在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名,但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法,它们可能被称为htmlEscape、HTMLEscape或escapeHTML,但不会是escapeHtml。
2.2. 声明
声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。这一章我们重点讨论变量和类型的声明,第三章将讨论常量的声明,第五章将讨论函数的声明。
一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件。每个源文件中以包的声明语句开始,说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包,然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用)。例如,下面的例子中声明了一个常量、一个函数和两个变量:
gopl.io/ch2/boiling
// Boiling prints the boiling point of water.
package main
import "fmt"
const boilingF = 212.0
func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("boiling point = %g°F or %g°C\n", f, c)
// Output:
// boiling point = 212°F or 100°C
}
其中常量boilingF是在包一级范围声明语句声明的,然后f和c两个变量是在main函数内部声明的声明语句声明的。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问,而不是仅仅在其声明语句所在的源文件中访问。相比之下,局部声明的名字就只能在函数内部很小的范围被访问。
一个函数的声明由一个函数名字、参数列表(由函数的调用者提供参数变量的具体值)、一个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值,那么返回值列表是省略的。执行函数从函数的第一个语句开始,依次顺序执行直到遇到return返回语句,如果没有返回语句则是执行到函数末尾,然后返回到函数调用者。
2.3. 变量
var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。变量声明的一般语法如下:
var 变量名字 类型 = 表达式
其中“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。这个特性可以简化很多代码,而且可以在没有增加额外工作的前提下确保边界条件下的合理行为。例如:
var s string
fmt.Println(s) // ""
这段代码将打印一个空字符串,而不是导致错误或产生不可预知的行为。Go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。
也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导):
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
初始化表达式可以是字面量或任意的表达式。在包级别声明的变量会在main入口函数执行前完成初始化(§2.6.2),局部变量将在声明语句被执行到的时候完成初始化。
一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:
var f, err = os.Open(name) // os.Open returns a file and an error
2.4. 赋值
使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。
x = 1 // 命名变量的赋值
*p = true // 通过指针间接赋值
person.name = "bob" // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值
特定的二元算术运算符和赋值语句的复合操作有一个简洁形式,例如上面最后的语句可以重写为:
count[x] *= scale
这样可以省去对变量表达式的重复计算。
数值变量也可以支持++递增和--递减语句(译注:自增和自减是语句,而不是表达式,因此x = i++之类的表达式是错误的):
v := 1
v++ // 等价方式 v = v + 1;v 变成 2
v-- // 等价方式 v = v - 1;v 变成 1
2.5. 类型
变量或表达式的类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集等。
在任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。例如,一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符、或者一个月份;一个float64类型的变量可以用来表示每秒移动几米的速度、或者是不同温度单位下的温度;一个字符串可以用来表示一个密码或者一个颜色的名称。
一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。
type 类型名字 底层类型
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。
2.6. 包和文件
Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。
每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。
包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。
为了演示包基本的用法,先假设我们的温度转换软件已经很流行,我们希望到Go语言社区也能使用这个包。我们该如何做呢?
让我们创建一个名为gopl.io/ch2/tempconv的包,这是前面例子的一个改进版本。(这里我们没有按照惯例按顺序对例子进行编号,因此包路径看起来更像一个真实的包)包代码存储在两个源文件中,用来演示如何在一个源文件声明然后在其他的源文件访问;虽然在现实中,这样小的包一般只需要一个文件。
我们把变量的声明、对应的常量,还有方法都放到tempconv.go源文件中:
gopl.io/ch2/tempconv
// Package tempconv performs Celsius and Fahrenheit conversions.
package tempconv
import "fmt"
type Celsius float64
type Fahrenheit float64
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
转换函数则放在另一个conv.go源文件中:
package tempconv
// CToF converts a Celsius temperature to Fahrenheit.
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// FToC converts a Fahrenheit temperature to Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
每个源文件都是以包的声明语句开始,用来指明包的名字。当包被导入的时候,包内的成员将通过类似tempconv.CToF的形式访问。而包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。要注意的是tempconv.go源文件导入了fmt包,但是conv.go源文件并没有,因为这个源文件中的代码并没有用到fmt包。
因为包级别的常量名都是以大写字母开头,它们可以像tempconv.AbsoluteZeroC这样被外部代码访问:
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
要将摄氏温度转换为华氏温度,需要先用import语句导入gopl.io/ch2/tempconv包,然后就可以使用下面的代码进行转换了:
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
在每个源文件的包声明前紧跟着的注释是包注释(§10.7.4)。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释(译注:如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释)。如果包注释很大,通常会放到一个独立的doc.go文件中。
2.7. 基础数据类型
整型
Go内置了12种整数类型,分为有符号整型和无符号整型两大类:
整数类型 备注 有符号 int 所占用的字节数与运行机器的CPU相关。在32位机器中,大小为4字节;在64位机器中,大小为8字节 int8 占用一个字节存储(8位),范围是-128 ~ 127 int16 占用两个字节存储(16位),范围是-32768 ~ 32767 int32 占用四个字节存储(32位),范围是-2147483648 ~ 2147483647 int64 占用八个字节存储(64位),范围是-9223372036854775808 ~ 9223372036854775807 无符号 uint 所占用的字节数与运行机器的CPU相关。在32位机器中,大小为4字节;在64位机器中,大小为8字节 uint8 占用一个字节存储(即8位),范围是0 ~ 255 uint16 占用两个字节存储(16位),范围是0 ~ 65535 uint32 占用四个字节存储(32位),范围是0 ~ 4294967295 uint64 占用八个字节存储(64位),范围是0 ~ 18446744073709551615 uintptr 一个无符号整数类型 byte byte类型是uint8的别名 byte和uint8 byte是uint8的别名,两者底层结构相同。因此byte变量和uint8变量无需进行类型转换:
package main
import "fmt"
func main() { var number1 uint8 = 23 var number2 byte number2 = number1 fmt.Println(number2) // output: 23 } 类型转换 除别名外,不同类型的整型必须进行强制类型转换,就算是底层结构相同的不同类型,也必须进行强制类型转换。例如在64位的机器中,int和int64的底层结构是一致的,但还是要进行强制类型转换,而不能直接赋值。
package main
import "fmt"
type myInt int
func main() { var number1 int64 = 3 var number2 int var number3 myInt
number2 = int(number1)
number3 = myInt(number2)
fmt.Println(number1, number2, number3)
// output: 3, 3, 3
} 支持的操作 整型支持算术运算和位操作,算术表达式和位操作表达式的结果还是整型。
package main
import "fmt"
func main() { var a int = (1 + 2) * 3 var b int32 = 1000 >> 2 fmt.Println(a, b) // output: 9, 250 } 浮点型 浮点型用于表示包含小数点的数据,Go语言内置两种浮点数类型,分别是float32和float64。其中浮点数字面量被自动推断为float64。
package main
import "fmt"
func main() { var number1 = 3.14 number2 := 1.23
var number3 float32 = 3.12
var number4 float64 = 23.567
fmt.Printf("%T, %T, %T, %T\n", number1, number2, number3, number4)
// output: float64, float64, float32, float64
} 精度 一个float32可以提供大约6个十进制精度,而float64可以提供大约15个十进制精度,因此优先使用float64类型。
数值范围 可以使用标准库中的常量math.MaxFloat32和math.MaxFloat64获取到float32和float64的最大值:
package main
import ( "fmt" "math" )
func main() { fmt.Println(math.MaxFloat32) // output: 3.4028234663852886e+38
fmt.Println(math.MaxFloat64)
// output: 1.7976931348623157e+308
} 类型 最大值 最小值 float32 3.4028234663852886e+38 1.4e-45 float64 1.7976931348623157e+308 4.9e-324 浮点型的比较 在计算机中很难进行浮点数的精确表示和存储,因此两个浮点数之间不应该使用 == 和 != 进行比较操作。高精度的科学计算可以使用math标准库来完成。
复数 Go语言内置了两种复数类型,分别是complex64和complex128。复数在计算机中使用两个浮点数来表示,一个表示实部,另一个表示虚部。其中complex64由两个float32构成,complex128由两个float64构成。复数的字面量表示和数学表示法一致,复数字面量默认类型是complex128。
package main
import "fmt"
func main() { number := 3.1 + 6i fmt.Printf("%v, %T\n", number, number) // output: (3.1+6i), complex128 } 构造复数 可以使用内置函数complex来构造复数。
package main
import "fmt"
func main() { number := complex(2.1, 3) fmt.Printf("%v, %T\n", number, number) // output: (2.1+3i), complex128
number2 := complex(float32(2.1), 3)
fmt.Printf("%v, %T\n", number2, number2)
// output: (2.1+3i), complex64
} 获取实部和虚部 使用内置函数real和imag可以获取复数的实部和虚部:
package main
import "fmt"
func main() { number1 := complex128(23 + 6i) real1 := real(number1) img1 := imag(number1) fmt.Printf("real: %v, real type: %T, image: %v, image type: %T\n", real1, real1, img1, img1) // output: real: 23, real type: float64, image: 6, image type: float64
number2 := complex64(23 + 6i)
real2 := real(number2)
img2 := imag(number2)
fmt.Printf("real: %v, real type: %T, image: %v, image type: %T\n", real2, real2, img2, img2)
// output: real: 23, real type: float32, image: 6, image type: float32
} 字符串 Go语言将字符串作为一种原生的基本数据类型。字符串是一个不可修改的数据类型。它的底层结构如下:
type stringStruct struct { str unsafe.Pointer // 指向底层字节数组的指针 len int // 字节数组长度 } 初始化 字符串的初始化可以使用字符串字面量:
var s = "hello world" 访问内部成员 字符串是常量,可以通过索引访问其字节单元。但不能修改某个字节的值。
package main
import "fmt"
func main() { var s = "hello world" fmt.Printf("%v, %T\n", s[0], s[0]) // output: 104, uint8
s[0] = "H"
// panic: cannot assign to s[0] (strings are immutable)
} 字符串切片 基于字符串创建的切片和原字符串指向相同的底层数组,一样不可修改。对字符串的切片操作返回的结果仍然是字符串string,而不是slice。
package main
import ( "fmt" )
func main() { sentence := "hello world!"
sub1 := sentence[0:4]
sub2 := sentence[:4]
fmt.Printf("sub1: %s, %T\n", sub1, sub1)
// output: sub1: hell, string
fmt.Printf("sub2: %s, %T\n", sub2, sub2)
// output: sub2: hell, string
} 与切片的转换 使用类型转换可以将字符串直接转换为byte切片和rune切片。但注意当字符串数据量很大时,使用该方法要慎重,因为转换过程中需要全部复制字符串的内容。
使用强制类型转换也可以将[]byte与[]rune转换为字符串。
package main
import "fmt"
func main() { var s = "hello world"
// string to []byte
bytes := []byte(s)
fmt.Println(bytes)
// output: [104 101 108 108 111 32 119 111 114 108 100]
// string to []rune
runes := []rune(s)
fmt.Println(runes)
// output: [104 101 108 108 111 32 119 111 114 108 100]
// []byte to string
s1 := string(bytes)
fmt.Println(s1)
// output: hello world
// []rune to string
s2 := string(runes)
fmt.Println(s2)
// output: hello world
} 获取字符串长度 内置的len函数可以获取字符串长度:
package main
import "fmt"
func main() { sentence := "Hello" fmt.Println(len(sentence)) // output: 5 } 遍历 字符串的遍历与切片类似,使用range遍历时返回的是一个int类型表示下标和int32类型表示字符的Unicode码:
package main
import "fmt"
func main() { demo := "hi" for index, character := range demo { fmt.Printf("index: %T, %v; character: %T, %v\n", index, index, character,character) } // outputs: // index: int, 0; character: int32, 104 // index: int, 1; character: int32, 105 } 还有更常规一些的遍历方式,遍历下标,并通过下标访问字符串中的每个成员:
package main
import "fmt"
func main() { demo := "abc" for i := 0; i < len(demo); i++ { fmt.Printf("%c", demo[i]) } // output: abc } 拼接 使用“+”可以拼接字符串:
package main
import "fmt"
func main() { s1 := "abc" s2 := "def" result := s1 + s2 fmt.Println(result) // output: adcdef } byte & rune Go内置两种字符类型:一种是byte的字节类型(byte也是uint8的别名),另一种是表示Unicode编码的字符rune。rune在Go的内部是int32类型的别名,占用4个字节。