# Go 语言入门教程
Go 语言实现了开发效率与执行效率的完美结合,让你像写 JavaScript 代码(效率)一样编写 C 代码(性能)。
## 语言结构
```go
// 必须在源文件中非注释的第一行指明这个文件属于哪个包
// 每个应用程序都包含一个名为 main 的包
package main
import "fmt"
// 每一个可执行程序都必须包含一个 main 函数
// 一般来说都是启动后第一个执行的函数(如果有 init 函数则先执行 init 函数们)
func main() {
fmt.Println("Hello, World")
}
```
### 注释
* 单行注释 `// ...` 是最常见的注释形式
* 多行注释 `/* ... */` 也叫块注释,一般用于 包的文档描述 或 注释成块的代码片段
## 包
以包为基本单位的代码组织方式
* *文件夹相对路径 == 包路径*,不同文件夹下的源文件无法共用一个包名,因为文件夹路径不一样
* *包名 == 文件夹名*,按照约定 注1,包名与导入路径(import path)的最后一个元素一致,如 `math/rand` 的包名就是 `rand`
* *没有文件的概念*,同一个包的内容可以随意分布在不同 **源文件** 内,编译器不关心。
注1:包名可以跟文件夹名不一样,编译不会报错,但容易引起误解,所以一般都是同名的。
#### 包 Packages
Go 应用由 **包** 组成,并从 `main` 包的 `main` 方法开始执行。
```golang
import (
// Go 标准包
"fmt"
// 第三方包
"github.com/spf13/pflag"
// 匿名包
_ "github.com/jinzhu/gorm/dialects/mysql"
// 内部包
"github.com/marmotedu/iam/internal/apiserver"
)
```
#### 导入 Imports
```golang
// 几种特殊的 import 用法
import (
. "fmt" // no name, import in scope
File "io/ioutil" // rename ioutil to File
_ "net" // net will not be available, but init() inside net package will be executed
)
```
```go
// 支持一个依赖的多个大版本并存
```
#### 导出 Exported names
首字母大写的都会导出,外部可见,首字母小写的不导出。 如 `Pizza` 是 exported name, 而 `pizza` 则是 unexported name。
## 函数
#### 函数 Functions
```golang
func add(x int, y int) int {
return x + y
}
```
注:对应的 TypeScript 版本。可以看到 Go 基本就是把 `:` 省略掉了而已。
```ts
function add(x: number, y: number): number {
return x + y;
}
```
当多个参数类型相同时,前面的类型声明可省略,保留最后一个即可。如 `x int, y int` 可写成 `x, y int`。
```golang
func add(x, y int) int {
return x + y
}
```
#### 多个返回值 Multiple results
一个函数可返回多个结果:
```golang
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
```
#### 命名返回值 Named return values
返回值可被命名,它们会被视作 定义在函数顶部的变量。
没有参数的 return 语句返回已命名的返回值。
```golang
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // 同 return x, y
}
func main() {
fmt.Println(split(17)) // 输出 7 10
}
```
#### 函数值
函数也是值。它们可以像其它值一样传递。
函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。
```golang
func add() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
```
#### 嵌套函数
nested function vs named function
* Recursion: Nested functions cannot directly call themselves recursively.
* Performance: Defining nested functinos can have a slight impact on performance compared to defining functions at the package level. This is because each invocation of the outer function will create a new instance of the nested function.
```go
func foo() {
var bar func()
bar = func() { // 这里不支持写 `func bar()`
if rand.IntN(5) > 1 {
bar()
}
}
bar()
}
```
#### 代码块
*全域代码块 > 包代码块 > 函数代码块 > 语句、子语句代码块 > 空代码块*
在 Go 语言中,*代码块一般就是一个由花括号括起来的区域,里面可以包含表达式和语句*。Go 语言本身以及我们编写的代码共同形成了一个非常大的代码块,也叫 **全域代码块**。
这主要体现在,只要是公开的全局变量,都可以被任何代码所使用。相对小一些的代码块是代码包,一个代码包可以包含许多子代码包,所以这样的代码块也可以很大。
接下来,每个包也都是一个代码块,每个函数也是一个代码块,每个 if语句、for语句、switch语句 和 select语句 都是一个代码块。甚至,switch 或 select语句 中的 case子句 也都是独立的代码块。
走个极端,我就在 main函数 中写一对紧挨着的花括号算不算一个代码块?当然也算,这甚至还有个名词,叫 **空代码块**。
注:Go 中花括号除了可以表示代码块,还可以表示数据结构,如 interface struct 中的使用,以及赋值时的使用场景。
#### 作用域
一个程序实体的作用域总是会被限制在某个代码块中,而这个作用域最大的用处,就是对程序实体的访问权限的控制。
对“高内聚,低耦合”这种程序设计思想的实践,恰恰可以从这里开始。
```golang
package main
import "fmt"
var block = "package"
func main() {
fmt.Printf("The block is %s.\n", block) // package
block := "function"
{
block := "inner"
fmt.Printf("The block is %s.\n", block) // inner
}
fmt.Printf("The block is %s.\n", block) // function
}
```
## 变量
#### 变量 Variables
`var` 语句用于声明一个变量列表。`var` 语句可出现在 包或函数级别。
```golang
var c, python, java bool
var i, j = 1, 2 // 声明变量并立即赋值时,类型声明可省略
func main() {
var i int // 等同于 `var i int = 0` 或 `i := 0`
fmt.Println(i, c, python, java) // 输出 0 false false false
}
```
#### 短变量声明 Short variable declarations
在函数中,简洁赋值语句 `:=` 可替换原 `var` 声明。但不支持在函数外使用。
```golang
func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
fmt.Println(i, j, k, c, python, java)
}
```
```go
var i int = 0 // 这种场景有以下三种简写形式
var i int // 省掉 零值
var i = 0 // 省掉 类型 (类型推断:找出值对应的默认类型)
i := 0 // 省掉 var
```
#### 值类型和引用类型
* 值类型赋值给另外一个变量时会进行值拷贝
* 函数直接传参的逻辑同赋值,都是值拷贝
* *值类型拷贝数据本身,引用类型拷贝数据对应的内存地址*。指针本身是值类型,值为一个内存地址。
map 和 channel 等最初是 pointer 类型,后面独立出来引用类型的概念。引用类型实例本身可看成是一个 struct,包含了实例的元信息,如 slice 的 `{length int; cap: int; ptr: *E}`。也就是说,拷贝的是元信息但不包含底层实际数据,简化下,可以理解为,*引用类型赋值拷贝的是指针的值*(即,实际数据的内存地址)。
> Why are maps, slices, and channels references while arrays are values?
>
> There's a lot of history on that topic. Early on, maps and channels were syntactically pointers and it was impossible to declare or use a non-pointer instance. Also, we struggled with how arrays should work. Eventually we decided that the strict separation of pointers and values made the language harder to use. Changing these types to act as references to the associated, shared data structures resolved these issues. This change added some regrettable complexity to the language but had a large effect on usability: Go became a more productive, comfortable language when it was introduced.
#### 基本类型 Basic types
`int`, `uint` 和 `uintptr` 在 32 位系统上通常为 32 位,在 64 位系统上则为 64 位。
当你需要一个整数值时应使用 `int` 类型,除非你有特殊的理由使用固定大小或无符号的整数类型。
类型推导会使用 `int` `float64` `complex128`
```golang
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 别名
rune // int32 别名, 表示一个 Unicode 码点 (code point)
float32 float64
complex64 complex128
```
本例展示了几种类型的变量。同导入语句一样,变量声明也可以 *分组* 成一个语法块。
```golang
package main
import (
"fmt"
"math/cmplx"
)
var (
ToBe bool = false
MaxUint uint64 = 1<<64 - 1 // MaxInt 则为 1<<63 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
func main() {
fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe) // Type: bool Value: false
fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt) // Type: uint64 Value: 18446744073709551615
fmt.Printf("Type: %T Value: %v\n", z, z) // Type: complex128 Value: (2+3i)
}
```
#### 零值 Zero values
未赋初始值的变量都会获得对应的 zero value
* `0` for numeric types
* `false` for the boolean type
* `""` (the empty string) for strings
#### 类型转换 Type conversions
The expression `T(v)` converts the value `v` to the type `T`.
Go 不像 C,所有类型转换必须显式转换。
```golang
i := 42
f := float64(i)
var f2 float64 = i // 报错,必须显式转换类型
u := uint(f)
```
```
// golang
string(97) // "a"
// javascript
String(97) // "97"
```
#### 类型推断 Type inference
在声明一个变量而不指定其类型时(即,使用不带类型的 `:=` 语法或 `var =` 表达式语法),变量的类型由右值推导得出。
在声明变量且赋值时,可利用 Go 语言自身的类型推断能力,省掉变量的类型声明。类型推断是一种编程语言在 *编译期* 自动解析表达式类型的能力。
Go 语言的类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它恰恰可以避免散弹式的代码修改),更不会损失程序的运行效率。
#### 常量 Constants
常量使用 `const` 声明。
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。(注意与 JS 中的 `const` 作区分)
```golang
const (
Big = 1 << 100 // Create a huge number by shifting a 1 bit left 100 places
Small = Big >> 99 // Shift it right again 99 places, so we end up with 1<<1, or 2
)
```
##### iota
第九个希腊字母,在 Go 中为一个特殊常量,可简单理解为 `const` 语句块中的行索引。
注:`iota` 仅适用于 `const`,无法在 `var` 定义块中使用。
```go
// 定义枚举值
const (
a = iota + 1 // 1
b // 2
c
)
```
#### 变量的重声明
变量重声明就是对已经声明过的变量再次声明。变量重声明的前提条件有
* 变量的类型不能变,类型在变量初始化时就已经确定了
* 变量的重声明只能发生在某一个代码块中。如果代码块中的变量跟外层的变量重名,那不叫重声明
* 变量的重声明只有在使用短变量声明时才会发生,否者无法通过编译
* 被“声明并赋值”的变量必须是多个,并且其中至少有一个是新变量
变量重声明其实算是一个语法糖(或者叫便利措施)。它允许我们在使用短变量声明时不用理会被赋值的多个变量中是否包含旧变量。
```golang
var err error
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n") // err 被重声明并赋值
```
读者评论(异议)
所谓“变量的重声明”容易引发歧义,而且也不容易理解。如果没有为变量分配一块新的内存区域,那么用声明是不恰当的。在《Go 语言圣经》一书中将短声明的这种特性称为赋值。
Go 为短声明语法提供了一个语法糖(或者叫便利措施):*短变量声明不需要声明所有在左边的变量。如果多个变量在同一个词法块中声明,那么对于这些变量,短声明的行为等同于赋值*。
## 字符串
```go
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
```
```go
func main() {
row := `[a-z]` // row string 常用于写正则
s := "abc"
r := 'a' // rune
b := s[0] // byte
s2 := s[:1] // string
fmt.Println(r, b, s2) // 97 97 a
r == 'a' // true
b == 'a' // true
b == r // ERROR: mismatched types byte and rune
b == "a" // ERROR: mismatched types byte and untyped string
r == "a" // ERROR: mismatched types rune and untyped string
}
```
Unicode(统一码、万国码)是一种字符编码标准,它为世界上几乎所有的字符分配了唯一的标识符。`rune` 即为一个Unicode字符。
UTF-8 是一种 Unicode 字符编码方案,是 Unicode 的一种实现方式,使用可变长度的编码方式,兼容 ASCII 编码(ASCII字符使用单个字节存储)。一个字节编码的码点范围 0-7F,同 ASCII 编码表;两个字节编码的码点范围 80-7FF,主要是一些拉丁文;中文的码点范围是 4E00-9FFF,使用3个字节进行编码。
```go
func main() {
str := "⌘" // 只有 1 个 rune,但 len(str) == 3
for i := 0; i < len(str); i++ { // str[i] 为 byte 类型
fmt.Printf("%d:%x, ", i, str[i]) // 0:e2, 1:8c, 2:98,
}
for _, r := range str { // r 为 rune 类型
fmt.Printf("%x\n", r) // prints 2318
}
}
```
## 流程控制语句 Flow control statements
### `for` 循环
Go 只有 `for` 循环,没有 `while` 循环。且 `for` 循环没有 `()`。
循环中 init statement 定义的变量只在循环体内可见。
```golang
for i := 0; i < 10; i++ {
fmt.Print(i)
}
```
```golang
// 常规用法
func main() {
sum := 1
for ; sum < 1000; { // 初始化语句; 条件表达式; 后置语句
sum += sum
}
fmt.Println(sum)
}
// for 就是 while
func main() {
sum := 1
for sum < 1000 { // *同时*省略 初始化语句 和 后置语句 时,`;` 也可以省略
sum += sum
}
fmt.Println(sum)
}
// 无限循环
func main() {
for { // 循环条件也可以省,这就成了无限循环
// ...
}
}
```
### `if` 语句
`if` 语句与 `for` 循环类似,表达式外无需小括号 `( )`,而大括号 `{ }` 则是必须的。
同 `for` 一样, `if` 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 `if` 之内(含 `else` 部分)。
```golang
func main() {
i := 0
if i < 10 { i++ } else { i-- } // 表达式外没有 `( )`(可以有但没必要)
if j := 5; i < 10 { i += j } // if 也可以有初始化声明
fmt.Println(i)
}
```
### `switch` 语句
`switch` 语句可以在条件表达式前执行一个简单的语句。另,case 后跟布尔判断时,没有条件表达式。
Go 会自动提供每个 `case` 后面的 `break` 语句,除非以 `fallthrough` 语句结束,否则分支会自动终止。
另外,`switch` 的 `case` 无须为常量,且取值不必为整数。
```golang
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin": fmt.Println("OS X")
case "linux": fmt.Println("Linux")
default: fmt.Printf("%s", os)
}
}
// 利用 switch 简化长 if-then-else 链 // JS 写法 `switch (true) { case ... }`
func main() {
t := time.Now()
switch { // 相当于 switch true
case t.Hour() < 12: fmt.Println("Good morning!")
case t.Hour() < 17: fmt.Println("Good afternoon.")
default: fmt.Println("Good evening.")
}
}
// break 和 fallthrough 的用法
switch {
case true:
fmt.Println("1.1") // 执行
fallthrough
case false:
fmt.Println("1.2") // 执行,跟在 fallthrough 后面的 case 语句始终会执行
fallthrough
case false:
fmt.Println("1.3") // 执行
break
fallthrough // 无效,因为 break 会终止 switch 语句
case true:
fmt.Println("1.4") // 不会执行
}
switch {
case true:
fmt.Println("2.1") // 执行
case true:
fmt.Println("2.2") // 不会执行
}
```
#### 单个 case 包含多个选项
```go
switch fruit {
case "apple", "orange":
fmt.Println("It's either an apple or an orange.")
case "banana", "pineapple":
fmt.Println("It's either a banana or a pineapple.")
default:
fmt.Println("It's something else.")
}
```
#### 缩进
为啥 `case` 不用缩进
* The cases are logically labels, many people put labels at the same indentation level as the block they are in. [☍](https://stackoverflow.com/questions/4509039/why-the-strange-indentation-on-switch-statements)
* Syntactically, `case` and `default` labels belong in the same category as `goto` labels. Section 6.8.1 of the C11 standard has the following definition: [☍](https://stackoverflow.com/questions/29023601/can-i-put-code-outside-of-cases-in-a-switch)
```txt
labeled-statement:
identifier : statement
case constant-expression : statement
default : statement
```
实际在 `switch` 和 `case` 之间也是不能插入代码的,JavaScript 和 Golang 都是一样的,会报错。
```go
var x = 1
func main() {
switch x {
fmt.Println("init") // syntax error: unexpected fmt, expecting case or default or }
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("default")
}
}
```
JavaScript 中 label 的用法 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label
```js
foo: {
console.log('face');
break foo;
console.log('this will not be executed');
}
console.log('swap');
// 输出
// "face"
// "swap"
```
### `defer` 语句
```go
defer foo()
defer func() { /* ... */ }()
```
推迟调用的函数,*参数会立即求值*,但直到外层函数返回前该函数都不会被调用。
推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照 *后进先出* 的顺序调用。
注:即使 defer 后面的代码 panic 了,也不会影响 defer 语句的执行。
```golang
func main() {
defer fmt.Print("world")
for i := 1; i < 3; i++ { defer fmt.Print(i) }
fmt.Print("hello")
}
// 输出 hello321world
```
`defer` 对返回值的影响:Deferred anonymous functions may access and modify the surrounding function’s named return parameters.
```go
// In this example, the foo function returns "Change World"
func foo() (result string) {
defer func() {
fmt.Println(result) // "Hello World"
result = "Change World" // change value at the very last moment
}()
return "Hello World"
}
```
## 复杂类型 More types: structs, slices, and maps
### 指针 Pointers
指针保存内存地址。与 C 不同,Go 没有指针运算。
类型 `*T` 是指向 `T` 类型值的指针。其零值为 `nil`。
`&` 操作符会生成一个指向其操作数的指针。
> Go 语言中比较让人费解的设计:`*ptr` 用在代码中表示 ptr 指针指向的值,而当 `*Person` 作为类型时,表示指向 Person 的指针,同样的 `*xx` 出现在代码中和出现在类型中表示的意义正好要倒一倒。助记:`*` 类比 算术运算符 `-`
```golang
func main() {
i, j := 42, 27
var p *int
fmt.Println(p) //
p = &i // point to i
fmt.Println(p) // 0xc00018c008
*p = 21 // set i through the pointer
fmt.Println(i) // 21
p = &j // point to j
fmt.Println(p) // 0xc00018c010
*p = *p / 3 // divide j through the pointer
fmt.Println(j) // 9
fmt.Println(p) // 0xc00018c010
}
```
### 结构体 Structs
```txt
StructType = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName [ TypeArgs ] .
Tag = string_lit .
```
一个结构体 `struct` 就是一组字段 field 的集合。A `struct` is a collection of fields.
```golang
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2} // 创建
v.X = 4 // 访问
fmt.Println(v.X)
// 结构体指针 Pointers to structs
p := &v
p.X = 1e9 // (*p).X 的简写形式,但数组就得老实写 (*p)[0]
fmt.Println(v)
// 结构体字面量 Struct Literals
var (
// 直接列出字段的值来新分配一个结构体
v1 = Vertex{1, 2} // has type Vertex {1 2}
// 使用 `Name:` 语法可以仅列出部分字段,此时字段名的顺序随意
v2 = Vertex{X: 1} // Y:0 is implicit {1 0}
v3 = Vertex{} // X:0 and Y:0 {0 0}
p = &Vertex{1, 2} // has type *Vertex &{1 2}
)
}
```
#### embedded field
Go supports embedding of structs and interfaces to express a more seamless composition of types.
An embedding looks like a field without a name.
```go
// Defines a struct named MyMutex which has two fields: count of type int and sync.Mutex.
// The sync.Mutex field is an embedded field,
// which means that the MyMutex struct inherits all the methods of the sync.Mutex struct.
type MyMutex struct {
count int
sync.Mutex
}
```
```go
type base struct {
num int
}
func (b base) describe() string {
return fmt.Sprintf("base with num=%v", b.num)
}
type Container struct {
str string
base // 如果改成 `b base` 的话,就不支持直接调用,必须 `co.b.num` 而无法 `co.num`
}
func main() {
co := Container{
base: base{num: 1},
str: "some name",
}
// We can access the base’s fields directly on co, e.g. co.num.
fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str)
// Alternatively, we can spell out the full path using the embedded type name.
fmt.Println("also num:", co.base.num)
fmt.Println("describe:", co.describe())
}
```
嵌入指针类型的用法:
```go
type Container struct {
str string
*base // 修改点 1
}
func main() {
co := Container{
base: &base{num: 1}, // 修改点 2
str: "some name",
}
// ... 其他用法不变
}
```
#### struct tag
A **struct tag** is additional meta data information inserted into struct fields. The meta data can be acquired through *reflection*. Struct tags usually provide instructions on *how a struct field is encoded to or decoded from a format*.
```go
type Person struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
MiddleName string `json:"middle_name,omitempty"`
}
```
As mentioned in the documentation of [reflect.StructTag](http://golang.org/pkg/reflect/#StructTag), by convention the value of a tag string is a space-separated list of key:"value" pairs, for example:
```go
type User struct {
Name string `json:"name" xml:"name"`
}
```
示例,利用 `reflect` 包获取 tag 信息:
```go
func main() {
p := Person{}
t := reflect.TypeOf(p)
field := t.Field(2)
fmt.Println(field.Tag.Get("json"))
}
// Output:
// middle_name,omitempty
```
### 数组 Arrays
数组:类型确定,长度不可变
值类型:传递参数的时候进行值拷贝
类型 `[n]T` 表示拥有 `n` 个 `T` 类型的值的数组。
数组的长度是其类型的一部分,因此 *数组不能改变大小*。切片则为数组元素提供 *动态大小的、灵活的视角*,切片比数组更常用。
#### 初始化数组
如果数组长度不确定,可以使用 `...` 代替数组的长度,编译器会根据元素个数自行推断数组的长度:
```go
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
```
我们还可以通过指定下标来初始化元素:
```go
// 将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}
// 考点:字面量初始化切片时候,可以指定索引,没有指定索引的元素会在前一个索引基础之上加一
var x = []int{2: 2, 3, 0: 1} // [1 0 2 3]
```
#### 强类型示例
数组的长度不同,被认为是不同类型,无法直接比较。int + float64 也会报错,强类型语言和弱类型语言差别还是蛮大的。
```go
func main() {
a := [2]int{5, 6}
b := [3]int{5, 6}
if a == b { // invalid operation: a == b (mismatched types [2]int and [3]int)
fmt.Println("equal")
} else {
fmt.Println("not equal")
}
}
```
### 切片 Slices
切片 slice:是对底层数组的一段内容的引用。
本质:实际数组 (pointer) + 长度 (len) + 最大容量 (cap)。
直接改数组内容或者改切片内容,实际都是改的数组,都是联动的。
```golang
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a)
// Slices 切片操作
arr := [6]int{2, 3, 5, 7, 11, 13}
var s []int = arr[1:4] // 半开区间,含 1 不含 4
arr[2] = 55
fmt.Println(s) // [3 55 7]
}
```
可以通过「切片字面量 Slice literals」直接新建切片。
```golang
[3]bool{true, true, false} // 数组字面量,数组类型为 `[n]T` n 是固定死的
[]bool{true, true, false} // 切片字面量,切片类型为 `[]T` 表明切片是动态的
```
切片时可忽略上下界。如忽略,下界默认为 0,上界则默认为该*切片的长度*。
```golang
var a [10]int
// 以下几项都是等效的
a[0:10]
a[:10]
a[0:]
a[:]
```
```go
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[3:4:5] // 还支持第三个参数,用于控制 capacity
// 取值范围 0 <= low <= high <= capacity <= cap(a)
fmt.Println(s, len(s), cap(s)) // 输出 [4] 1 2
}
```
切片拥有 长度 和 容量,可通过表达式 `len(s)` 和 `cap(s)` 来获取。
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
```golang
func main() {
s := []int{2, 3, 5, 7, 11, 13}
s = s[:0] // Slice the slice to give it zero length
printSlice(s) // len=0 cap=6 []
s = s[:4] // Extend its length, 只可以往后扩展,不能往前扩展,如 [-1:] 这样不行
printSlice(s) // len=4 cap=6 [2 3 5 7]
s = s[2:] // Drop its first two values
printSlice(s) // len=2 cap=4 [5 7]
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
```
切片的零值是 `nil`。nil 切片的长度和容量为 0 且没有底层数组。
```golang
func main() {
var ns []int
fmt.Println(ns, len(ns), cap(ns))
if ns == nil { fmt.Println("nil!") }
}
```
切片可包含任何类型,甚至包括其它的切片。
```golang
// 二维切片
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
```
切片可以用内建函数 `make` 来创建,这也是你 *创建动态数组* 的方式。
```golang
func main() {
a := make([]int, 5) // len=5 cap=5 [0 0 0 0 0]
b := make([]int, 0, 5) // len=0 cap=5 []
c := b[:2]; // len=2 cap=5 [0 0]
d := c[2:5]; // len=3 cap=3 [0 0 0]
}
```
内建的 `append` 函数为切片追加新的元素(不是修改现有切片,而是返回修改后的新切片),`append` 的结果是一个包含原切片所有元素加上新添加元素的切片。
当 s 的底层数组太小,不足以容纳所有给定的值时,它就 *会分配一个更大的数组*,返回的切片会指向这个新分配的数组 *所以这里存在行为二分性,应该是一个潜在的坑*。
```golang
var s []int
// 切片会按需增长
s = append(s, 0) // len=1 cap=1 [0]
// 可以一次性添加多个元素
s = append(s, 1, 2, 3) // len=4 cap=4 [0 1 2 3]
```
```golang
// append 的行为存在二分性,编码时要注意避坑
// append 不是修改原切片而是返回一个 新的切片
func main() {
s := make([]int, 1, 2)
s1 := append(s, 1)
s2 := append(s, 2, 3)
s1[0] = 7
s2[0] = 8
fmt.Println(s) // [7]
fmt.Println(s1) // [7 1]
fmt.Println(s2) // [8 2 3]
}
```
切片与性能:不要在 for 循环中使用 append
```go
// file: slice_test.go
// run test: go test -bench=. -benchmem
// append 版本 vs 一次性申请版本,前者耗时和内存分配都是后者的3~5倍
func appendSpeed(n int) {
var s []int
for i := 0; i < n; i++ {
s = append(s, i)
}
}
func appendSpeed2(n int) {
s := make([]int, n)
for i := 0; i < n; i++ {
s[i] = i
}
}
func BenchmarkAppendSpeed(b *testing.B) {
appendSpeed(b.N)
}
func BenchmarkAppendSpeed2(b *testing.B) {
appendSpeed2(b.N)
}
```
```go
// 实现队列时,可以采用循环队列来规避 append 操作
type CircularQueue struct {
data []int
head int
tail int
size int
}
// ...
```
切片与内存泄漏:截取 slice 容易导致内存泄漏
```go
func main() {
s := []*Node{node1, node2}
s = s[:1] // 此时 node1 在代码中已经无法访问了
// 但 s 的底层数组没动,也就还保留着对 node1 的引用,导致 node1 相关的内存都无法被释放
// 通过新建 slice 规避内存泄漏
s = append([]*Node{}, s[:1]...)
}
```
### 系列 Range
Go 语言中 `range` 关键字用于 `for` 循环中迭代数组(array)、切片(slice)、通道(channel)或映射(map)的元素。在数组和切片中它返回 `(index, value)`,在映射中返回 `(key, value)` 对。
```golang
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
// 可以将下标或值赋予 _ 来忽略它
for _, value := range pow {
fmt.Print(value)
}
// range 也可以用在 map 的键值对上
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
```
在迭代过程中修改被迭代对象,值对象(只有数组)取的 k v 不会受影响,其他的都受影响。
```go
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
for i, v := range a { // range 表达式是副本参与循环,即,这里参与循环的是 a 的副本
if i == 0 {
a[1] = 12 // 这里的修改不会影响后续迭代中 v 的取值
a[2] = 13
}
r[i] = v
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
}
// Output:
// r = [1 2 3 4 5]
// a = [1 12 13 4 5]
func main() {
var a = [5]int{1, 2, 3, 4, 5}
for i, v := range &a { // 这里改成 &a,指针的副本依旧是指向原数组的指针,所以迭代过程中会被实时影响到
if i == 0 { // 或者把 a 由 数组 改成 切片,也能起到同样的效果
a[1] = 12 // 这里的修改会影响后续迭代中 v 的取值
a[2] = 13
}
fmt.Print(v, " ")
}
}
// Output: 1 12 13 4 5
```
```go
func main() {
var m = map[string]int{"A": 21, "B": 22, "C": 23}
counter := 0
for k, v := range m {
clear(m)
counter++
fmt.Println(k, v)
}
fmt.Println("counter is ", counter)
}
// Output:
// <第一条 k v 随机输出>
// counter is 1
```
```go
func main() {
var s = []int{1, 2, 3}
for i, v := range s {
clear(s)
fmt.Print(i, v, "\n")
}
}
// Output: 0 1 1 0 2 0
```
```go
func main() {
var a = []int{1, 2, 3, 4, 5}
var r = make([]int, 0)
for i, v := range a {
if i == 0 {
a = append(a, 6, 7) // 这里的 a 换了地址,不影响 range a 中的地址,故不影响迭代过程
}
r = append(r, v)
}
fmt.Println(r) // [1 2 3 4 5]
}
```
`range` 迭代次数根据类型定,如果只有类型不取值就不会报错。
```go
// 正常输出 0 1 2
for k := range (*[3]int)(nil) {
println(k)
}
// panic: runtime error: invalid memory address or nil pointer dereference
for k, v := range (*[3]int)(nil) {
println(k, v)
}
```
`range` 迭代数组 `[n]T` 和 指向数组的指针 `*[n]T` 效果是一样的,但 `**[n]T` 就报编译错误了
```go
type T struct{ n int }
func main() {
ts := [2]T{}
for i, t := range &ts { // 数组的指针 &ts 和 数组 ts 在这里没啥差异
t.n = i // 这里的 t 依然是数组元素的副本,所以修改 t.n 不会影响数组元素的值
}
fmt.Print(ts) // [{0} {0}]
}
```
### 映射 Maps
Go | JavaScript | Ruby | Python
-----|--------------|--------|----------
Map | Object | Hash | Dict
映射将键映射到值。`make` 函数会返回给定类型的映射,并将其初始化备用。
```golang
type Vertex struct {
Lat, Long float64
}
// 使用 make 创建
var m map[string]Vertex = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
// 通过映射字面量创建
var m = map[string]Vertex{
"Bell Labs": Vertex{40.68433, -74.39967},
"Google": {37.42202, -122.08408}, // 若顶级类型只是一个类型名,可以省略
}
```
修改映射
```golang
// 插入或修改元素
m[key] = elem
// 获取元素
elem = m[key] // 不支持 e.someKey 这种 JavaScript 下的用法,这是由强类型语言性质决定的
// 删除元素
delete(m, key)
// 通过双赋值检测某个键是否存在
elem, exist = m[key]
```
`map` 相关考题 —— 寻址
```go
type Person struct {
Name string
Age int
}
func main() {
p := Person{"John", 30}
dict1 := make(map[string]Person)
dict2 := make(map[string]*Person)
dict1["John"] = p // 这里复制了一份 p 到 map 内,后续 map 内的 struct 只能整体替换,无法部分修改
dict2["John"] = &p
dict1["John"].Age = 33 // ERROR: UnaddressableFieldAssign
dict2["John"].Age = 33 // OK
}
```
```go
type Math struct {
x, y int
}
var m = map[string]Math{
"foo": Math{2, 3},
}
func main() {
m["foo"].x = 4 // cannot assign to struct field m["foo"].x in map
// struct 是值类型,需要整体替换,如 `foo = m["foo"]; foo.x = 4; m["foo"] = foo`
fmt.Println(m["foo"].x)
}
// 作为对比,这里的用法是没问题的
func main() {
m := make(map[string]int)
m["foo"]++ // OK
fmt.Println(m["foo"])
}
```
`map` 相关考题 —— 指针取值
```go
func main() {
m := make(map[string]string)
p := &m
m["foo"] = "bar"
p["hello"] = "world" // invalid operation: cannot index p
(*p)["hello"] = "world" // OK
}
```
`map` 相关考题 —— 有效 key
Map lookup requires an equality operator ... struct、数组、指针 和 信道 等都能作为键名,但 slice 不行(暂未定义相等行为)。
```go
func main() {
m := make(map[[1]int]int)
a := [1]int{1}
b := [1]int{1}
m[a] = 100
fmt.Println(m[b]) // 100
}
```
Map VS Struct
* Map
- All keys must be the same type
- All values must be the same type
- Keys are indexed, we can iterate over them
- Reference Type
- Use to represent a collenction of related properties
- Don't need to know all the keys at compile time
* Struct
- Values can be of different type
- Keys don't support indexing
- Value Type
- You need to know all the different fields at compile time
- Use to represent a "thing" with a lot of different properties
## 方法和接口 Methods and interfaces
### 方法
方法就是一类带特殊的 **接收者** 参数的函数。
```go
type T struct{}
func (T) foo() {}
func (*T) bar() {}
type S struct{ *T }
func main() {
t := T{}
t.foo() // foo 的类型为 func (T).foo() 所以 t 实际也是 foo 的一个参数,只是写在 foo 的前面罢了
t.bar() // bar 的类型为 func (*T).bar() 所以实际会展开成 (&t).bar()
}
```
Go 没有类,但可以为结构体类型定义方法,实现类似的功能。
*只能为在同一包内定义的类型的接收者声明方法*。而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。
```golang
type Vertex struct {
X, Y float64
}
// 定义 Vertex.Abs() 方法
func (v Vertex) Abs() float64 { // 基于惯例,实例变量一般取类型首字母,如这里的 `v`
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (Vertex) dummy() { // 如果方法体内无需对结构体进行操作,那么结构体变量名可省略
fmt.Println("I do nothing with Vertex struct.")
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
```
#### 指针接收者
**指针接收者** 的方法可以修改接收者指向的值。由于方法经常需要修改它的接收者,**指针接收者** 比 **值接收者** 更常用。
对于方法,不管定义时的接收者是值还是指针,*使用时传值或指针都能正常编译*。但普通函数没有这个便利,不能混用。
使用指针接收者的原因
* 方法能够修改其接收者指向的值
* *可以避免在每次调用方法时创建副本*(复制该值)
```golang
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) { // 如果去掉这里的 * 则 Scale 只会对 v 的副本进行操作
v.X = v.X * f // 对于 struct 支持 (*v).X 简写为 v.X,编译时会自动转换
v.Y = v.Y * f
}
// 对于方法,不管定义时的接收者是值还是指针,使用时传值或指针都能正常编译
func main() {
v := Vertex{3, 4}
v.Scale(10) // 实际被解析为 (&v).Scale(10)
p := &v
a := p.Abs() // 实际被解析为 (*p).Abs()
fmt.Println(a)
}
```
当「值接收者」和「指针接收者」碰到 接口 时:
* 不允许同时定义 值接收者 和 指针接收者 的同名方法 (开发规范:要么全是指针接收者要么全是值接收者)
* 方法定义为「值接收者」时,传指针不报错,因为可以(隐式地)通过指针拿到值
* 方法定义为「指针接收者」时,传值报错,因为传值是复制,无法根据当前值拿到原始值的地址(定义为「指针接收者」说明方法的原意是要改原值值内容的)
```go
type Person interface{ Say() }
type Man struct{}
func (m Man) Say() {}
type Woman struct{}
func (w *Woman) Say() {}
func main() {
m := Man{}
var p1 Person = m // OK
var p2 Person = &m // OK 方法定义为「值接收者」时,传指针不报错;反之报错
// the pointer is implicitly dereferenced
w := Woman{}
var p3 Person = &w // OK
var p4 Person = w // cannot use w (variable of type Woman) as Person value:
// Woman does not implement Person (method Say has pointer receiver)
}
```
知识点:方法表达式。通过类型引用的方法表达式会被还原成普通函数样式,接收者是第一个参数,调用时显式传参。类型可以是 `T` 或 `*T`,只要目标方法存在于该类型的方法集中就可以。
知识点:方法值。当指针值赋值给变量或者作为函数参数传递时,会立即计算并复制该方法执行所需的接收者对象,与其绑定,以便在稍后执行时,能隐式地传入接收者参数。
```go
type N int
// 本例中,接收者为值,所以绑定的是值
func (n N) test(i int) {
fmt.Println(n, i)
}
func main() {
var n N = 10
// 方法表达式 演示
N.test(n, 4) // 10 4
(*N).test(&n, 5) // 10 5
// 方法值 演示
n++
f1 := n.test // 此时 f1 对应的 n 被固化在 n 的当前值 11
n++
f2 := (&n).test // 这里的 `&n` 最终还是会被转成 `n` 的,这个是由方法定义决定的
n++
f1(6) // 11 6
f2(7) // 12 7 // 注意这里不是 13
}
```
```go
type N int
// 本例中,接收者为指针,所以绑定的是指针
func (n *N) test() {
fmt.Println(*n)
}
func main() {
var n N = 10
n++
f1 := n.test
n++
f2 := (&n).test
f1() // 12
f2() // 12
}
```
#### Pass by Value
定义方法使用 **值接收者** 这种形式时,需要很清楚地知道传的的值属于 **值类型** 还是 **引用类型**,如果是引用类型,那效果跟方法的 **指针接收者** 这种用法是一样的。
* Value Type
- `int`, `float`, `string`, `bool`, structs, arrarys
- use pointers to change these things in a function
* Reference Types (传的值本质上是一个 指针)
- slices, maps, channels, pointers, functions
- don't worry about pointers with these types
```go
func main() {
m := make(map[string]int)
m["age"] = 28
n := m // 复制的是指针
n["age"] = 29
fmt.Printf("m ptr: %p, val: %[1]v\n", m)
fmt.Printf("n ptr: %p, val: %[1]v\n", n)
type Base struct {
num int
}
b := Base{num: 1}
c := b // 复制的是值
c.num = 2
fmt.Printf("b: %d\n", b)
fmt.Printf("c: %d\n", c)
}
// Output:
// m ptr: 0xc0000740c0, val: map[age:29]
// n ptr: 0xc0000740c0, val: map[age:29]
// b: {1}
// c: {2}
```
#### 更多例子
```go
type People struct{}
func (p *People) ShowA() {
println("people showA")
p.ShowB() // 注意这里,本例中输出的是 "people showB" 而不是 "teacher showB"
}
func (p *People) ShowB() {
println("people showB")
}
type Teacher struct {
People
}
func (t *Teacher) ShowB() {
println("teacher showB")
}
func main() {
t := Teacher{}
t.ShowA()
t.ShowB()
}
// Output:
// people showA
// people showB
// teacher showB
```
### 接口
> Purpose of Interface: 代码复用
接口类型 是由一组方法签名定义的集合。接口类型的变量可以保存任何实现了这些方法的值。
```golang
type Printable interface {
Print() string
}
```
```go
package main
import (
"fmt"
)
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() { // 行内的 nokiaPhone 可省略
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone) // 也可改成 NokiaPhone{}
phone.call()
phone = new(IPhone)
phone.call()
}
```
#### 隐式实现
注:这一点跟 TypeScript 的鸭式辨型法(duck typing)比较类似
Concrete Type VS Interface Type: 无法基于「接口」创建特定实体,因为 接口 不是 实体类型。
类型通过实现一个接口的所有方法来实现该接口,无需显式声明,自然也就不需要 implements 关键字了。
隐式实现从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。
优缺点
* 优点:少写模板代码; 可以随时新增接口定义而不需要修改现有代码
* 缺点:因为是隐式实现,不够直观,且无法得到及时反馈,类型错误需要等到编译时才能看到报错
```golang
package doc
type Document struct {
Filename string
}
func (doc *Document) Print() string {
return doc.Filename
}
```
```golang
import . "xxx/doc"
type Printable interface {
Print() string
}
func Print(p Printable) {
fmt.Println(p.Print())
}
func main() {
var doc = Document{"hero"}
Print(&doc)
}
```
How can I guarantee my type satisfies an interface?
You can ask the compiler to check that the type `T` implements the interface `I` by attempting an assignment using the zero value for `T` or pointer to `T`, as appropriate:
```go
type T struct{}
var _ I = T{} // Verify that T implements I.
var _ I = (*T)(nil) // Verify that *T implements I.
```
If `T` (or `*T`, accordingly) doesn't implement `I`, the mistake will be caught at compile time.
#### 接口值
接口值保存了一个具体底层类型的具体值。接口值调用方法时会执行其底层类型的同名方法。
*`nil` 接口值* 既不保存值也不保存具体类型。注意与 *接口值(类型+值)的值为 nil* 区分开。
```golang
func (doc *Document) Print() string {
if doc == nil {
return "nil"
}
return doc.Filename
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
func main() {
var p Printable
// 接口值为 nil
describe(p) // (, )
// Print(p) // 运行时报错
var doc *Document
p = doc
// 接口值的具体值为 nil
describe(p) // (, *main.Document)
Print(p) // 运行时能正确找到 (*Document).Print 方法
}
```
#### 空接口
指定了零个方法的接口值被称为 **空接口**,空接口可保存任何类型的值。
```golang
type any = interface{} // 1.18 新增
```
#### 类型断言
A type assertion provides access to an interface value's underlying concrete value.
```golang
// 断言接口值 i 保存的具体值的类型是 T,并将其底层类型为 T 的值赋给变量 v
// 如果断言能够成立,那么 ok 为 true 否者为 false
// 如果这里没有定义 ok 变量,那么断言失败就会抛错
v, ok := i.(T) // 这里的 i 必须是一个 interface,否则编译报错
```
```go
t: = i.(T) // If `i` does not hold a `T`, the statement will trigger a panic
t, ok := i.(T) // If not, `ok` will be false and `t` will be the zero value of type T, no panic
```
```golang
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s) // hello
s, ok := i.(string)
fmt.Println(s, ok) // hello true
f, ok := i.(float64)
fmt.Println(f, ok) // 0 false
f = i.(float64) // 报错(panic)
fmt.Println(f)
}
```
**类型选择** 是一种按顺序从几个断言中选择分支的结构。
```golang
switch v := i.(type) { // 这里的 type 是关键字,不是变量名
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}
```
```golang
func handleInput(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("%v is int\n", v)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
```
```golang
// 动态类型(variant types aka algebriac types)(即实现 TypeScript 的 `x: number | string`
func (x any) {
switch x.(type) {
case string:
//...
case int:
//...
}
}
```
#### Embedded interface
```go
type Reader interface {
Read(p []byte) (n int, err error)
Close() error
}
type Writer interface {
Write(p []byte) (n int, err error)
Close() error
}
// ReadWriter's methods are Read, Write, and Close.
type ReadWriter interface {
Reader // includes methods of Reader in ReadWriter's method set
Writer // includes methods of Writer in ReadWriter's method set
}
```
#### 接口使用示例 `fmt.Stringer` `error` `io.Reader`
`fmt` 包中定义的 `Stringer` 是最普遍的接口之一。fmt 包(还有很多包)都通过此接口来打印值。
```golang
// fmt.Stringer 是一个可以用字符串描述自己的类型
type Stringer interface {
String() string // 差不多就是 JS 中的 toString()
}
```
```golang
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
dent := Person{"Dent", 42}
fmt.Println(dent) // Dent (42 years)
}
```
Go 程序使用 error 值来表示错误状态。`error` 类型是一个内建接口。
通常函数会返回一个 error 值,为 `nil` 时表示成功;非 `nil` 表示失败。
```golang
type error interface {
Error() string
}
func main() {
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
}
```
## 泛型 Generic
Generics are a way of writing code that is independent of the specific types being used. Functions and types may now be written to use any of a set of types.
Generics and three new big things to the language:
* Type parameters for functions and types
* Type sets defined by interfaces
* Type inference
### 类型参数 Type Parameters
Functions and types are now permitted to have type parameters. A type parameter list looks like an ordinary parameter list, except that it *uses square brackets* instead of parentheses.
泛型函数
```go
import (
"fmt"
"golang.org/x/exp/constraints"
)
func Min[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}
func main() {
x := Min[int](2, 3) // explicit type argument
y := Min(2.0, 3.0) // type inference
fMin := Min[float64] // get a non-generic function by instantiating Min
z := fMin(2.0, 3.0)
fmt.Println(x, y, z)
}
```
泛型类型
```go
type Tree[T any] struct {
left, right *Tree[T]
value T
}
func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }
var stringTree Tree[string]
```
**泛型特化 instantiation** 是指将泛型代码转换为具体类型的代码。在 Go 标准编译器中(不同 compiler 可能有不同的行为),泛型特化是在编译期间完成,避免了运行时的类型检查和类型转换,从而提高代码的性能和运行效率。
### 联合类型 Type sets
Type constraints must be interfaces. Interfaces as type sets is a powerful new mechanism and is key to making type constraints work in Go.
```go
// define the type set containing the types int64 and float64
type Number interface {
~int64 | ~float64
}
```
`~` is a new token added to Go. `~T` means the set of all types with underlying type T. For example, the expression `~string` means the set of all types whose underlying type is `string`. This includes the type `string` itself as well as all types declared with definitions such as `type MyString string`.
Interfaces used as constraints may be given names (such as Ordered), or they may be *literal interfaces inlined* in a type parameter list. For example:
```go
// Here S must be a slice type whose element type can be any type.
[S ~[]E, E any] // 简化写法
[S interface{~[]E}, E interface{}] // 完整写法
// Scale returns a copy of s with each element multiplied by c.
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
r := make(S, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}
```
## 错误处理
```go
package main
import (
"errors"
"fmt"
"math"
)
func Sqrt(value float64) (float64, error) {
if value < 0 {
// errors.New(s string) 来返回错误,有点 JS `new Error()` 的意思
return 0, errors.New("math: negative number passed to Sqrt")
}
return math.Sqrt(value), nil
}
func main() {
result, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(result)
}
result, err = Sqrt(9)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(result)
}
}
```
Go 的 `err` 在最后,Node.js 的 `err` 在最前面。
```js
import { readFile } from 'node:fs';
readFile('/etc/passwd', (err, data) => {
if (err) throw err;
console.log(data);
});
```
## 测试
Go 的测试库非常简单,且没有提供断言 assertion 能力。*Go 推荐利用语言本身的能力 + 外加清晰的报错信息 来写单元测试*。
Create a new file ending in `_test.go` in the same directory as your package sources.
```go
var fmtTests = []struct{fmt string; val any; out string}{
{"%d", 12345, "12345"},
{"%v", 12345, "12345"},
}
func TestSprintf(t *testing.T) {
for _, tt := range fmtTests {
s := Sprintf(tt.fmt, tt.val)
if s != tt.out {
t.Errorf("Sprintf(%q, %v) = %q want %q", tt.fmt, tt.val, s, tt.out)
}
}
}
```
Run `go test` in that directory. That script finds the Test functions, builds a test binary, and runs it.
## 性能分析
Profile
* CPU 分析:在 runtime 中每隔很短的时间,记录当前正在运行的协程的栈。持续一段时间之后,通常是5~10s。通过分析这段时间记录下来的栈,出现频率比较高的函数占用CPU比较多。
* Mem 分析:内存分析只能分析在堆上申请内存的情况,同CPU类似,也是采用采样的方法,每一定次数的内存申请操作会采样一次。通过分析这些采样的记录可以判断出哪些语句申请内存较多。
Go语言内置了获取程序运行数据的工具,包括以下两个标准库:
* `runtime/pprof` 采集工具型应用运行数据进行分析
* `net/http/pprof` 采集服务型应用运行时数据进行分析
```go
import _ "net/http/pprof"
func main() {
// 浏览器访问 http://localhost:8888/debug/pprof/ 就可以看到性能分析信息了
http.ListenAndServe(":8888", nil)
}
```
### 工具型应用
收集数据
```go
import (
"log"
"os"
"runtime"
"runtime/pprof"
"time"
)
func main() {
// Collect CPU profile
f, err := os.Create("profile.pprof")
if err != nil {
log.Fatal(err)
}
defer f.Close()
err = pprof.StartCPUProfile(f)
if err != nil {
log.Fatal(err)
}
defer pprof.StopCPUProfile()
// Collect memory profile
f, err = os.Create("mem_profile.pprof")
if err != nil {
log.Fatal(err)
}
defer f.Close()
runtime.GC() // Run garbage collection to get accurate memory profile
err = pprof.WriteHeapProfile(f)
if err != nil {
log.Fatal(err)
}
// Your code goes here
for i := 0; i < 100; i++ {
hello()
time.Sleep(100 * time.Millisecond)
}
}
func hello() {
println("hello")
}
```
分析,输入一下命令后,可以进一步输入 `top` `list` `web` 等进行分析。
```bash
# analyze the CPU profile
$ go tool pprof profile.pprof
# analyze the memory profile
$ go tool pprof -alloc_objects mem_profile.pprof
```
## 其他
Web服务端模型
```go
for {
conn, err := l.Accept()
// ...
go serve(conn)
}
```
接受一个新的TCP连接,创建一个新的协程用于读写该连接上的数据。标准库 net/http, thrift 等都是用的这种服务端模型。
相对于进程和线程,协程更加轻量级,创建和退出一个协程的代价比较小。
但是这种方式在请求处理阻塞的情况下,容易引发“协程爆炸”,进而引发内存占用过多,调度延迟等问题。