本文是看了Go官方文档后进行的练习和总结。
Go富有表现力、简洁、干净并且高效 。它的并发机制 让我们能容易地编写充分利用多核和联网机器的程序,它的类型系统 可以实现灵活和模块化的程序构建。Go可以快速编译为机器代码 ,并且还有垃圾回收 的便利性以及运行时反射 的强大功能。它是一种快速、静态类型的编译语言,就像是一种动态类型的解释型语言。
富有表现力在于代码很直观,比如:
go
time.Sleep(10 * time.Second)
很直观地知道我是要睡眠10秒。
简洁:包名比如fmt等都很简单直接。
干净:比如引入不使用的包,在go中认为是一种错误,保存代码时会自动删掉引入却没有使用的import语句。
高效:自带并发编程,充分利用多核。
安装
-
点击下载地址中的下载按钮下载安装包。
-
双击下载好的安装包,根据提示进行安装。
这个安装包会把Go发行版安装在
/usr/local/go
这个文件中,并且设置/usr/local/go/bin
路径到PATH
环境变量中。需要重启终端会话来让这个改变生效。 -
验证是否已经安装好Go了:
shell$ go version go version go1.20.5 darwin/amd64
创建模块
-
创建一个文件夹并cd到文件目录下:
go$ mkdir practice && cd practice
-
初始化模块
使用go mod init 模块名称初始化模块,这会创建一个
go.mod
文件,这个文件可以用于对代码进行依赖追踪,模块名称就是模块的路径。当模块中导入了其他模块中的包时,通过go.mod
文件追踪提供这些包的模块。实际开发中,模块路径通常是源码所在的仓库位置。例如:
github.com/gin-gonic/gin
。shell$ go mod init example.com/practice go: creating new go.mod: module example.com/practice
此时
go.mod
文件的内容是:shellmodule example.com/practice go 1.20
-
创建一个属于
main
包的文件practice.go
,当运行main
包的时候,就会默认执行其中的main
函数。gopackage main // 用package关键字声明名为main的包 import "fmt" // 用import关键字导入fmt包 func main() { fmt.Println("Hello") // 打印字符串Hello }
-
package
(包)是组织函数的方式,包由同一个目录下的所有文件组成。 -
fmt
包含格式化文本并打印到控制台的函数。fmt包是标准库包中的一个,安装Go时自带的包。 -
当运行
main包
的时候,包中的main函数
会默认执行。
在
main
包所在的文件目录下运行go run . 表示编译和运行当前目录下的main
包。go$ go run . Hello
(执行
go help
可以查看Go的指令列表。) -
-
导入外部的包
gopackage main import ( "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 }
在代码中导入了外部的包,但是此时这个包并不存在于我们自己的项目代码中。执行:
shell$ go mod tidy
会找到并且下载
"github.com/gin-gonic/gin"
模块,默认情况下,它会下载最新版本。Go会添加
gin
模块作为一个依赖,并且新增了一个go.sum
文件用来验证模块。当go命令下载模块的时候,会计算加密哈希并将其与已知值进行比较,以验证文件自首次下载以来没有更改。如果下载的文件没有正确的哈希值,go命令会报告安全错误。(防止下载的包以及缓存的包被恶意篡改)
-
还可以执行
go mod vendor
指令将模块中包含的依赖复制到项目的vendor
文件夹中。
注释
注释作为程序的文档使用。有两种格式的注释:
- 行内注释:从
//
开始,在行末结束。 - 普通注释:从
/*
开始,以接下来遇到的第一个*/
作为结束。
注释不能在rune
和string
字面量中,注释不能包含注释。一个不包含换行符的普通注释就像空白。其他注释就像换行符。
标识符
标志符命名程序的实体,比如变量或者类型。一个标志符由一个或者多个字母和数字组成,标识符的第一个位置必须是一个字母。
下面这四个都是有效的标识符。
go
a
_x9
ThisVariableIsExported
αβ
最后一个标志符看起来有点奇怪用的是阿拉伯符号,因为在Go中letter(字母),指的是分类为字母分类的Unicode代码点,和下划线符号("_"
),不仅仅是26个英文字母。
所以可以写出下面这样的代码(一般不这样写):
go
package main
import "fmt"
func main() {
开始吃饭 := true
if 开始吃饭 {
吃米饭("小明")
}
}
func 吃米饭(姓名 string) {
fmt.Println(姓名 + "吃米饭")
}
关键字
下面的关键字被保留了,不能用作标识符。也就是说在我们为变量、函数、类型等命名的时候,不能使用下面这些单词。
go
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
常量
有布尔常量、rune常量、整数常量、浮点数常量、复数常量和字符串常量。
rune是int32类型的别名,可以理解为单个的字符。
常量可以显式地给到一个类型,或者隐式地给到一个类型。未给到类型的常量的默认类型可能是bool
、rune
、int
、float64
、complex128
、string
中的一个。
在常量声明中,iota
表示从0开始的连续整数常量。它的值是常量声明语句所在的索引,从0开始。
go
package main
import (
"fmt"
"reflect"
)
func main() {
const Pi float64 = 3.14159265358979323846
const zero = 0.0 // untyped floating-point constant
const (
size int64 = 1024
eof = -1 // untyped integer constant
)
const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", untyped integer and string constants
const u, v float32 = 0, 3 // u和v的类型都是float32 u = 0.0, v = 3.0
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Partyday // 6
numberOfDays // 7 this constant is not exported
)
const d = 1 - 0.707i
const e = 'e'
fmt.Println(reflect.TypeOf(zero)) // float64
fmt.Println(reflect.TypeOf(eof)) // int
fmt.Println(reflect.TypeOf(c)) // string
fmt.Println(reflect.TypeOf(Monday)) // int
fmt.Println(reflect.TypeOf(d)) // complex128
fmt.Println(reflect.TypeOf(e)) // int32
}
变量
变量是一个值的存储位置。能存储什么值取决于变量的类型。
如果只是声明没有赋值,每个变量会被初始化为类型的零值。
变量的静态类型是声明时给到的那个类型。接口类型的变量还有一个动态类型,是运行时赋给变量的值的类型。
go
package main
import (
"fmt"
"reflect"
)
func main() {
var i int
var U, V, W float64
var k = 0
var x, y float32 = -1, -2
var (
j int
u, v, s = 2.0, 3.0, "bar"
)
var number, ok = returnTwoValue()
entries := map[string]int{
"小明": 100,
}
name := "小明"
var _, found = entries[name] // map lookup; only interested in "found"
// 静态类型是interface{},动态类型是int
var z interface{}
z = 1
fmt.Println(i) // 0
fmt.Println(U, V, W) // 0 0 0
fmt.Println(k) // 0
fmt.Println(x, y) // -1 -2
fmt.Println(j) // 0
fmt.Println(u, v, s) // 2 3 bar
fmt.Println(number, ok) // 1 true
fmt.Println(found) // true
fmt.Println(z, reflect.TypeOf(z)) // 1 int
}
func returnTwoValue() (int, bool) {
return 1, true
}
使用:=
可以把声明和赋值放在同一行:
go
a := 1
等同于:
go
var a int
a = 1
基本数据类型
基本数据类型有布尔类型、数字类型(整数类型、浮点数类型、复数类型)、字符串类型。
go
var a, b bool = true, false
var c int = 42
var d uint = 12_000 // 这里的下划线是为了可读性,不会改变字面量的值
var e int8 = 100
var f float32 = 1.2e2
var g float64 = 1.23
var h complex64 = 1.2 + 1.5i
var i byte = 255
var j rune = '哈'
var k string = "写点啥"
byte
是uint8
的别名,rune
是int32
的别名。
复合数据类型
复合数据类型就是基础数据类型的组合。
复合数据类型有数组、切片、结构体、指针、函数、接口、映射以及通道类型。
数组
数组是由单一类型元素组成的编号序列。
go
var a [3]string
a = [3]string{"x", "y", "z"}
var b = [...]byte{0x11, 0x20} // 使用[...]时,会根据值的内容推断出数组的长度
c, d := a[0], b[1]
var e [2]bool // [false false]
fmt.Println(len(a), len(b), a, b, c, d, e)
可以使用len
方法得到数组的长度,数组的长度是类型的一部分,是固定不变的。
通过0
到len(a) - 1
的整数索引,可以找到数组对应位置的元素。
未初始化的数组中的每个元素是元素类型的零值。
切片
切片是对一个底层数组的连续片段的描述,同时提供对底层数组的元素的编号序列的访问。
go
var abc = [5]int{0, 1, 2, 3, 4} // 长度为5的数组 [0 1 2 3 4]
/*
切取索引位置[1, 5)的数组的元素
此时切片a和数组abc共享存储,a[0] 和 abc[1] 获取的是同一个位置的值
*/
var a []int = abc[1:]
fmt.Println(a, abc, a[0], abc[1]) // [1 2 3 4] [0 1 2 3 4] 1 1
/*
因为切片和底层数组共享存储,当修改切片的值时,底层数组的值也会改变
*/
a[1] = 100
fmt.Println(a, abc) // [1 100 3 4] [0 1 100 3 4]
/*
len方法得到切片中的元素的个数
cap方法得到的是 元素的个数 + 底层数组超出切片部分的元素的个数
*/
var b []int = abc[1:3]
fmt.Println(len(b), cap(b)) // 2, 4
/*
同一个底层数组对应的切片共享的都是同样的存储
此时切片a 和 切片b 的底层数组都是 abc,当b[1]发生变化,会影响到a和abc
*/
b[1] = 200
fmt.Println(a, b, abc) // [1 200 3 4] [1 200] [0 1 200 3 4]
/*
当切片的容量超出底层数组时,会重新分配底层数组,所以切片的底层数组不一定总是同一个数组
*/
var c []int = abc[2:4]
c = append(c, []int{1, 2, 3, 4, 5}...) // 向c切片中新增5个元素,由于超过了底层数组的容量,会为c的重新分配底层数组,此时c的底层数组已经不是abc了
c[0] = 10000
fmt.Println(c, abc, len(c), cap(c)) // [10000 3 1 2 3 4 5] [0 1 200 3 4] 7 8
var d []string
fmt.Println(d == nil) // true
可以使用len
方法得到切片中元素的个数(切片的长度),与数组不同,切片的长度在执行的过程中可能会发生改变。
通过0
到len(a) - 1
的整数索引,可以找到数组对应位置的元素。
未初始化的切片的值是nil
。
切片和它的底层数组共享内存。
可以使用make
方法初始化切片:
go
make([]T, length, capacity)
make总是会分配一个新的底层数组给切片。以下两个表达式是相同的:
go
make([]int, 50, 100)
new([100]int)[0:50]
内置的函数new(T)
会在运行时为类型T
的变量分配内存,并且返回指向该变量的类型为*T
的值。
make(T)
会返回类型为T
的值。并且T
的核心类型只能是切片、映射或通道。
go
e := make([]int, 2, 5)
f := new([5]int)[0:2]
fmt.Println(len(e), cap(e), e) // 2 5 [0 0]
fmt.Println(len(f), cap(f), f) // 2 5 [0 0]
结构体
结构体是由命名元素(称为字段)组成的序列,每个字段有一个名字和一个类型。
go
// An empty struct.
struct {}
// A struct with 6 fields.
struct {
x, y int
u float32
_ float32 // padding
A *[]int
F func()
}
下划线的意思是这个位置有一个字段,但是我们不需要用这个字段,所以就丢弃这个字段。
一个字段声明了类型,但没有显式的字段名称,这种字段称为嵌入字段。一个嵌入字段必须是类型名称T
,或者指向非接口类型的类型名称*T
,并且T
本身不是一个指针类型。
go
// A struct with four embedded fields of types T1, *T2, P.T3 and *P.T4
struct {
T1 // field name is T1
*T2 // field name is T2
P.T3 // field name is T3
*P.T4 // field name is T4
x, y int // field names are x and y
}
嵌入字段中的字段和方法被称为提升(promoted)。
给到一个结构类型S
和一个定义类型T
,提升方法以下面的方式被包含到该结构的方法集合中:
- 如果
S
包含嵌入字段T
,S
和*S
的方法集都包含接收者为T
的提升方法。*S
的方法集还包含接收者为*T
的提升方法。 - 如果
S
包含嵌入字段*T
,S
和*S
的方法集都会包含接收者为T
或*T
的提升方法。
简单来说就是:
-
把类型
T
嵌入结构体类型S
之后,类型*S
会包含T
和*T
的方法,类型S
只有T
的方法。 -
把类型
*T
嵌入结构体类型S
之后,类型*S
和S
都会包含T
和*T
的方法。
(看完后面的函数和方法之后再回过头来看这里应该就能明白)
提升字段和普通字段的用法相同,除了不能在复合字面量中作为字段名称使用。
go
type A struct {
x int
y string
}
type B struct {
z []int
}
type C struct {
A
B
n bool
}
/*
结构体类型C中包含了嵌入字段A和B
A和B中的字段不能在复合字面量中作为字段名使用
这种用法是错误的:
c := C{
x: 1,
y: "写点啥",
z: []int{1, 2},
n: true,
}
需要这样使用:
*/
c := C{
A{1, "写点啥"},
B{z: []int{1, 2}},
true,
}
var c1 C
c1.x = 1
c1.y = "写点啥"
c1.z = []int{1, 2}
c1.n = true
/*
结构体字面量遵循以下规则:
1. 键必须是在结构体类型中定义的字段名称。
2. 没有包含任何键的元素列表必须以字段的声明顺序,给每个结构体字段一个元素。
3. 如果任何元素有一个键,那么每个元素都必须有一个键。
4. 包含键的元素列表不需要给每一个结构体字段元素,被忽略的字段的值是该字段的零值。
5. 一个字面量可以忽略元素列表,这样的字面量的值是它的类型的零值。
6. 为属于不同包的非导出字段指定元素是错误的。(只能为导出字段指定元素)
*/
a := A{1, "写点啥"}
b := B{z: []int{1, 2}}
var c2 C
c2.A = a
c2.B = b
c2.n = true
fmt.Println(c) // {{1 写点啥} {[1 2]} true}
fmt.Println(c1) // {{1 写点啥} {[1 2]} true}
fmt.Println(c2) // {{1 写点啥} {[1 2]} true}
一个字段声明可以跟着一个可选的字符串字面量标签(tag),它会成为相应的字段声明中所有字段的属性。
go
struct {
x, y float64 "" // an empty tag string is like an absent tag
name string "any string is permitted as a tag"
_ [4]byte "ceci n'est pas un champ de structure"
}
// A struct corresponding to a TimeStamp protocol buffer.
// The tag strings define the protocol buffer field numbers;
// they follow the convention outlined by the reflect package.
struct {
microsec uint64 `protobuf:"1"`
serverIP6 uint64 `protobuf:"2"`
}
未初始化的结构体的字段都是对应类型的零值。
go
type A struct {
x, y int
}
type B struct {
A
z bool
}
var a A
var b B
fmt.Println(a) // {0 0}
fmt.Println(b) // {{0 0} false}
因为空结构体占的内存为0,所以为了节省内存会使用空结构体struct{}{}
来占位,比如当一个映射中只需要知道键存不存在,而不关心值是什么的时候,可以用struct{}{}
作为键的值。
指针
指针类型表示指向给定类型的变量的所有指针的集合,这个变量的类型被称为指针的基本类型。未初始化的指针的值是nil
。
go
var a *int
b := 123
a = &b // 获取指向变量b的指针
*a = 234 // 修改变量b的地址存储的值
fmt.Println(a, b) // 0xc00001a0a8 234
c := 12345
a = &c
*a = 100
fmt.Println(a, c) // 0xc00001a0c0 100
var d *string
fmt.Println(d == nil) // true
函数
函数类型表示参数和结果类型一样的所有函数的集合。
go
func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)
未初始化的函数的值是nil
。
在参数和结果列表中,名称必须全部出现或全部省略。
参数列表和结果列表都必须在括号中,但有一个例外,当只有一个未命名的返回结果的时候,可以不用括号。
go
package main
import "fmt"
func main() {
var a func(int) int
a = func(x int) int {
return x + 2
}
fmt.Println(a(1)) // 3
b(1, []int{2, 3, 4, 5}...)
}
func b(x int, others ...int) {
fmt.Println(x)
for i, val := range others {
fmt.Println(i, val)
}
}
方法
如果函数有一个接收者,这种函数称为方法。
go
package main
import (
"fmt"
"math"
)
func main() {
p := Point{
x: 3,
y: 4,
}
fmt.Println(p.Length()) // 5
p.Scale(2)
fmt.Println(p.Length()) // 10
}
type Point struct {
x float64
y float64
}
func (p *Point) Length() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
func (p *Point) Scale(factor float64) {
p.x *= factor
p.y *= factor
}
下面这个部分的p *Point
就是接收者,
go
func (p *Point) Length() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
Length
方法是绑定在*Point
类型上的,但是我们在使用的时候直接使用了:
go
p := Point{
x: 3,
y: 4,
}
fmt.Println(p.Length()) // 5
p的类型是Point
而不是*Point
,也能正常调用Length
方法,这是因为Go为了方便用户使用,在背后作了转换,会将Point
类型转换为*Point
类型,然后调用方法。如果一个方法Point
是接收者,那用*Point
类型去调用该方法的时候,也会在背后将*Point
类型转换为Point
类型来调用Point
类型的方法。
如果接收者的基本类型是泛型,接收者必须为方法声明对应的类型参数。
go
type Pair[A, B any] struct {
a A
b B
}
func (p Pair[A, B]) Swap() Pair[B, A] { ... } // receiver declares A, B
func (p Pair[First, _]) First() First { ... } // receiver declares First, corresponds to A in Pair
如果类型定义中声明了类型参数,那么类型名称就表示一个泛型。泛型会在使用时被实例化。
泛型类似函数的形参和实参,定义的时候定义形参类型,使用的时候传入实参类型。
go
type List[T any] struct {
value T
next *List[T]
}
示例1:
当不使用泛型的时候,对于如下代码,
go
type Point struct {
x, y float64
}
func (p *Point) Length() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
如果要将float64
类型改为float32
类型,那么就需要重新写一个类型和函数:
go
type Point struct {
x, y float32
}
func (p *Point) Length() float32 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
如果使用泛型,则可以直接这样写:
go
package main
import (
"fmt"
"math"
)
type Point[T float32 | float64] struct {
x, y T
}
func (p *Point[T]) Length() T {
x := float64(p.x)
y := float64(p.y)
return T(math.Sqrt(x*x + y*y))
}
func main() {
p := Point[float32]{
x: 3,
y: 4,
}
fmt.Println(p.Length()) // 5
p1 := Point[float64]{
x: 6,
y: 8,
}
fmt.Println(p1.Length()) // 10
}
(一般main函数是放在前面的,这里为了将泛型相关的内容放在前面直观一点)
示例2:
go
package main
import "fmt"
func main() {
p := Pair[int, string]{
a: 100,
b: "写点啥",
}
fmt.Println(p) // {100 写点啥}
fmt.Println(p.First()) // 100
fmt.Println(p.Swap()) // {写点啥 100}
}
type Pair[A, B any] struct {
a A
b B
}
func (p Pair[A, B]) Swap() Pair[B, A] {
var result Pair[B, A]
temp := p.a
result.a = p.b
result.b = temp
return result
}
func (p Pair[First, _]) First() First {
return p.a
}
一些其他泛型示例:
go
类型形参列表 类型实参 类型实参替换类型形参之后
type parameter list type arguments after substitution
[P any] int int satisfies any
[S ~[]E, E any] []int, int []int satisfies ~[]int, int satisfies any
[P io.Writer] string illegal: string doesn't satisfy io.Writer
[P comparable] any any satisfies (but does not implement) comparable
any
(即interface{}
)表示任意类型,~[]E
表示底层类型为[]E
的类型,io.Writer
表示实现了Writer
方法的类型,comparable
表示可比较的类型。
接口
接口定义一个类型集合。
接口类型的未初始化变量的值是nil
。
基本接口
完全由方法列表定义类型集合的接口称为基本接口。
go
// A simple File interface.
interface {
Read([]byte) (int, error)
Write([]byte) (int, error)
Close() error
}
无论什么类型,只要包含上述接口中的三个方法,就是实现了该接口。
所有类型都实现了空接口类型interface{}
,空接口的别名是预声明类型any
。
go
type Locker interface {
Lock()
Unlock()
}
func (p T) Lock() { ... }
func (p T) Unlock() { ... }
类型T
实现了接口Locker
。
简单示例:
go
package main
import "fmt"
func main() {
// Process类型实现了接口Locker
var p Locker = Process{}
p.Lock()
a()
p.Unlock()
}
type Process struct{}
type Locker interface {
Lock()
Unlock()
}
func (p Process) Lock() {
fmt.Println("锁住了")
}
func (p Process) Unlock() {
fmt.Println("解锁了")
}
func a() {
fmt.Println("加锁解锁之间的代码")
}
嵌入接口
接口T
可能会使用一个接口类型名称E
作为一个接口元素。这称为T
中的嵌套接口E
。T
的类型集合就是实现了T
中声明的方法和E
中声明的方法的所有类型。
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
}
简单示例:
go
package main
func main() {
var r R = 1
var rw RW = 2
// 类型R实现了Reader接口
var ri Reader = r
// 类型RW实现了ReadWriter接口
var rwi ReadWriter = rw
// 以下这句会报错,因为R类型没有Write方法,没有实现ReadWriter接口
// R does not implement ReadWriter (missing method Write)
// var rwi1 ReadWriter = r
ri.Close()
rwi.Close()
}
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
}
// 类型R包含Read和Close方法
type R int
func (r R) Read(p []byte) (n int, err error) {
return 1, nil
}
func (r R) Close() error {
return nil
}
// 类型RW包含Read、Write和Close方法
type RW int
func (rw RW) Read(p []byte) (n int, err error) {
return 1, nil
}
func (rw RW) Write(p []byte) (n int, err error) {
return 1, nil
}
func (rw RW) Close() error {
return nil
}
一般接口
更一般的形式是,一个接口元素可能是任意类型T
,以及声明底层类型的类型~T
,或者类型的并集t1|t2|...|tn
。
go
// An interface representing only the type int.
interface {
int
}
// An interface representing all types with underlying type int.
interface {
~int
}
// An interface representing all types with underlying type int that implement the String method.
interface {
~int
String() string
}
// An interface representing an empty type set: there is no type that is both an int and a string.
interface {
int
string
}
包含类型列表的接口类型只能被用作类型约束,不能像普通类型一样使用。类型约束就是使用泛型的时候,类型参数的限制范围,比如type [T comparable] struct {}
中的comparable
就是类型约束,限制类型参数只能是可比较的类型。
简单示例:
go
package main
import (
"fmt"
"strconv"
)
func main() {
// 包含类型列表的接口类型只能被用作类型约束,不能像普通类型一样使用
// var i A = t 会报错:
// cannot use type A outside a type constraint: interface contains type constraints
var x MyType = 100
st := SomeType[MyType]{
x: x,
}
fmt.Println(st.x.String())
}
// 包含类型列表的接口类型只能被用作类型约束
type SomeType[T TypeConstraint] struct {
x T
}
type TypeConstraint interface {
~int
String() string
}
type MyType int
func (mt MyType) String() string {
return strconv.Itoa(int(mt))
}
映射
映射是一组未排序的元素,通过唯一的键进行索引。
未初始化的映射的值是nil
。
键类型必须能进行比较操作(==
和!=
),所以函数、映射和切片不能作为键。
go
// 创建一个空的映射
var a map[string]int = make(map[string]int)
// 通过len获取映射的长度
fmt.Println(len(a)) // 0
// 添加元素
a["first"] = 1
// 获取元素
fmt.Println(a["first"]) // 1
// 删除元素
delete(a, "first")
fmt.Println(a) // map[]
a["first"] = 1
a["second"] = 2
a["third"] = 3
// 遍历元素
for key, val := range a {
fmt.Println(key, val)
}
删除一个不存在的键不会报错,获取一个不存在的键会取得值对应的类型的零值:
go
a := make(map[string]int)
delete(a, "notExisted")
b := a["notExisted"]
fmt.Println(b) // 这里获取到的值是int类型的零值,0
// 要判断一个键是否存在,可以使用下面这种写法
c, existed := a["notExisted"]
fmt.Println(c, existed) // 0 false
通道
goroutine
在介绍通道之前先简单介绍一下Go语句:
go语句在同一地址空间内,将一个函数调用的执行作为一个独立的并发控制线程(goroutine)启动。
go后面的表达式必须是一个函数或方法调用。
与一般的调用不同,程序执行时不会等待被调用的函数执行完毕。函数会在一个新的goroutine中独立开始执行。当函数终止时,对应的goroutine也会终止。
go
go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)
channel
通道给并行执行的函数提供了一种通信的方式,通过发送和接收某个特定类型的值进行通信。
未初始化的通道的值是nil
。
可选的<-
操作符指定通道的方向,发送或接收。如果给到一个方向,通道就是定向的,否则就是双向的。通过赋值或者显式转换,一个通道可以被限制为只发送或者只接收。
这里的发送和接收是面对通道的,向通道发送就是发送,从通道里接收就是接收。而不是通道接收数据算作接收。
go
chan T // can be used to send and receive values of type T
chan<- float64 // can only be used to send float64s
<-chan int // can only be used to receive ints
可以使用make
函数初始化通道。
go
make(chan int, 100)
第二个参数是可选的,表示容量,容量设置的是通道的缓存大小。如果容量为0或者没有第二个参数,就表示是无缓存通道。否则这个通道就是缓存通道。
go
package main
import (
"fmt"
)
func main() {
c := make(chan int, 2)
/*
两个并发执行的函数,函数中包含向通道发送数字的语句
*/
go func(c chan int) {
fmt.Println("aaa")
c <- 1
}(c)
go func(c chan int) {
fmt.Println("bbb")
c <- 2
}(c)
fmt.Println(<-c, <-c) // 可能打印出1 2 或者2 1,因为并发执行,无法确定执行顺序
}
发送语句 :ch <- 3
,3的部分可以是一个表达式,在通信开始之前,表达式的值和通道都会被进行分析。
通信会阻塞直到能够进行发送:
-
无缓存通道只有在接收器准备好的时候才能处理发送。
-
缓存通道只有在缓存中还有空间的时候才能处理发送。
对一个已经关闭的通道发送会导致运行时错误。对一个nil
通道发送会永久阻塞。
接收语句 :<-c
会阻塞直到有值可以使用时。从一个nil
的通道接收会立即执行,给出之前接收过的值类型的零值。x, ok := <-c
中的第二个结果值ok
表示通信是否已经成功执行,ok
的值是true
如果接收到的值是被一个成功的发送操作发送到通道的,ok
的值是false
如果接收到的值是因为通道已经关闭并且为空的时候生成的一个零值。
通道可以使用内置的函数close
进行关闭。
go
package main
import "fmt"
func main() {
c := make(chan int, 2)
go producer(c)
consumer(c)
}
func producer(c chan int) {
for i := 0; i < 3; i++ {
c <- i
}
close(c)
}
func consumer(c chan int) {
for i := range c {
fmt.Println("从通道中取得得值", i)
}
}
consumer
函数的部分可以替换为:
go
func consumer(c chan int) {
ele, ok := <-c
for ok {
fmt.Println("从通道中取得得值", ele)
ele, ok = <-c
}
}
语句
空语句
空语句是一个空的位置,什么也不做。比如下面的for语句中两个冒号旁边的3个位置,就是空语句,什么也没有,什么也不做。
go
for ;; {
//...
}
标签语句
标签语句就是在语句前面加上标签名称和冒号,标签语句可以作为goto
、break
或者continue
语句的目标。
go
Error: log.Panic("error encountered")
上面这个语句就打上了名为Error的标签。
(具体的使用看后面的goto
、break
、continue
部分)
表达式语句
表达式语句就是表达式。有一元表达式或者二元表达式。
go
Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr = PrimaryExpr | unary_op UnaryExpr .
binary_op = "||" | "&&" | rel_op | add_op | mul_op .
rel_op = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op = "+" | "-" | "|" | "^" .
mul_op = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .
unary_op = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .
主表达式(Primary expression)包含在一元表达式中,示例如下:
go
x
2
(s + ".txt")
f(3.1415, true)
Point{1, 2}
m["foo"]
s[i : j + 1]
obj.color
f.p[i].x()
简单示例:
go
package main
import "fmt"
func main() {
var x uint8 = 1 // 1 是一个主表达式
var y uint8 = 2 // 2 是一个主表达式
var z uint8 = 3 // 3 是一个主表达式
a := x // x 是一个主表达式
b := ^x // ^x 是一个一元表达式 (^用作一元操作符时表示按位取反,用作二元操作符时表示按位异或)
c := y + z // y + z 是一个二元表达式
fmt.Println(a, b, c) // 1 254 5
fmt.Printf("%b\n", x) // 1
fmt.Printf("%b\n", ^x) // 11111110
}
if语句
if
语句根据一个布尔表达式的值决定两个分支的有条件执行。如果布尔表达式的值为真,会执行if
分支,否则执行else
分支。
go
if x > max {
x = max
}
布尔表达式的前面还可以写一个简单的语句,会在分析布尔表达式前执行:
go
if x := f(); x < y {
return x
} else if x > z {
return z
} else {
return y
}
switch语句
表达式switch
case
包含的表达式与switch
包含的表达式比较。忽略switch
表达式等同于布尔值true
。
使用fallthrough
将控制权转移给下一个case
。
go
package main
import (
"fmt"
)
func main() {
a(0) // s1
a(5) // s2
a(100) // s3
fmt.Println(b()) // 1
c(9, 8) // s2
c(10, 20) // s1
c(1, 2) // s3
c(2, 1) // s2
}
func a(tag int) {
switch tag {
default:
s3()
case 0, 1, 2, 3:
s1()
case 4, 5, 6, 7:
s2()
}
}
func s1() {
fmt.Println("s1")
}
func s2() {
fmt.Println("s2")
}
func s3() {
fmt.Println("s3")
}
func b() int {
// 没有switch表达式意味着true,case后面的表达式与true作比较
switch x := f(); {
case x < 0:
return -x
default:
return x
}
}
func f() int {
return -1
}
func c(x, y int) {
// 没有switch表达式意味着true,case后面的表达式与true作比较
switch {
case x > 10:
fallthrough // 将控制权转移给下一个case
case y > 10:
s1()
case x > y:
s2()
default:
s3()
}
}
类型switch
case
中的类型与switch
表达式中的类型比较。
go
package main
import (
"fmt"
)
func main() {
a(nil)
a(1)
a(1.2)
a(func(x int) float64 { return 1.3 })
a(true)
a(struct{}{})
}
func a(x any) {
switch i := x.(type) {
case nil:
fmt.Println("x is nil")
case int:
fmt.Println(i)
case float64:
fmt.Println(i)
case func(int) float64:
fmt.Println(i)
case bool, string:
fmt.Println("type is bool or string")
default:
fmt.Println("don't know the type")
}
}
类型switch
中不允许使用fallthrough
。
for语句
迭代可以由单个的条件、for
子句和range
子句控制。
单个条件:
go
for a < b {
a *= 2
}
只要a < b
一直成立,a *= 2
就会一直执行。
for子句:
go
for i := 0; i < 10; i++ {
f(i)
}
i := 0
是首次迭代前只执行一次的初始化语句;i < 10
是条件语句;i++
是每次代码执行结束之后执行的语句。
初始化语句(init statement),条件,后续语句(post statement)都可以省略。
go
for cond { S() } is the same as for ; cond ; { S() }
for { S() } is the same as for true { S() }
包含range子句的for语句:
go
范围表达式 返回的第一个值 返回的第二个值
Range expression 1st value 2nd value
array or slice a [n]E, *[n]E, or []E index i int a[i] E
string s string type index i int see below rune
map m map[K]V key k K m[k] V
channel c chan E, <-chan E element e E
简单示例:
go
package main
import (
"fmt"
)
func main() {
// 数组
var testdata *struct {
a *[7]int
}
for i, _ := range testdata.a {
// testdata.a is never evaluated; len(testdata.a) is constant
// i ranges from 0 to 6
fmt.Println(i)
}
var a [10]string
for i, s := range a {
// type of i is int
// type of s is string
// s == a[i]
fmt.Println(i, s)
}
// 切片
b := []int{1, 2, 3}
for i, val := range b {
fmt.Println(i, val)
}
// 字符串
c := "abc一二三"
for i, r := range c {
fmt.Printf("%d %c %v\n", i, r, r)
}
// 映射
var key string
var val interface{}
m := map[string]int{"mon": 0, "tue": 1, "wed": 2, "thu": 3, "fri": 4, "sat": 5, "sun": 6}
for key, val = range m {
fmt.Println(key, val)
}
// key == last map key encountered in iteration
// val == map[key]
// 通道
var ch chan int = producer()
for item := range ch {
fmt.Println(item, "从通道接收的值")
}
// 清空一个通道
// for range ch {
// }
}
func producer() chan int {
ch := make(chan int, 3)
for i := 0; i < 3; i++ {
ch <- i
}
close(ch)
return ch
}
go语句
go
语句在同一地址空间内,将一个函数调用的执行作为一个独立的并发控制线程(goroutine
)启动。
go
后面的表达式必须是一个函数或方法调用。
与一般的调用不同,程序执行时不会等待被调用的函数执行完毕。函数会在一个新的goroutine
中独立开始执行。当函数终止时,对应的goroutine
也会终止。
go
go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)
select语句
select
语句选择哪个可能的发送或接收操作会被处理。它和switch
语句看起来有点像,但是所有的case
都是指的通信操作。
因为nil
通道上的通信永远不会继续,只有nil
通道,没有default case
的select
语句会永远阻塞。
简单示例:
go
package main
import (
"fmt"
"time"
)
func main() {
a := make([]int, 2)
var i1, i2 int
c1 := make(chan int)
c2 := make(chan int)
c3 := make(chan int)
c4 := make(chan int)
go func() {
time.Sleep(1 * time.Second)
c1 <- 1
}()
go func() {
time.Sleep(1 * time.Second)
<-c2
}()
go func() {
time.Sleep(1 * time.Second)
c3 <- 3
}()
go func() {
time.Sleep(1 * time.Second)
c4 <- 4
}()
for i := 0; i < 4; i++ {
select {
case i1 = <-c1:
fmt.Println("received ", i1, " from c1")
case c2 <- i2:
fmt.Println("sent ", i2, " to c2")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Println("received ", i3, " from c3")
} else {
fmt.Println("c3 is closed")
}
case a[f()] = <-c4:
fmt.Println("received from c4")
// same as:
// case t := <-c4
// a[f()] = t
// 如果在这里加上default语句,因为每次for循环的时候,goroutine中的代码都没执行完成,所以每次都会走到default分支
// default:
// fmt.Println("no communication")
}
}
}
func f() int {
return 1
}
其他例子:
go
for { // send random sequence of bits to c
select {
case c <- 0: // note: no statement, no fallthrough, no folding of cases
case c <- 1:
}
}
select {} // block forever
return语句
函数F
中的return
语句终止函数F
的执行,并且可选地提供一个或多个结果值。所有F
中的defer
函数会在F
返回前执行。
go
func noResult() {
return
}
func simpleF() int {
return 2
}
func complexF1() (re float64, im float64) {
return -7.0, -4.0
}
func complexF2() (re float64, im float64) {
return complexF1()
}
// 如果结果参数声明了名称,那么return后面的值不是必须的,结果参数就像是本地变量一样
// return会返回这些变量
func complexF3() (re float64, im float64) {
re = 7.0
im = 4.0
return
}
func (devnull) Write(p []byte) (n int, _ error) {
n = len(p)
return
}
指定结果的return
语句在执行任何延迟函数之前设置结果参数。简单来说就是return
后面带表达式同时又有defer
语句的时候,return
后面的表达式先于defer
语句进行计算。
go
package main
import "fmt"
func main() {
fmt.Println(f()) // 函数中 defer函数2中 defer函数1中
}
func f() (a string) {
a = "函数中"
defer func() {
a += " defer函数1中"
}()
defer func() {
a += " defer函数2中"
}()
return
}
defer
函数先声明的后执行。
如果在return
的作用域中有一个和结果参数相同名称的不同实体,那么return
后面就不能什么都不加:
go
func f(n int) (res int, err error) {
// result parameter err not in scope at return
if _, err := f(n - 1); err != nil {
return // inner declaration of var err error
}
return
}
break语句
break
语句终止同一个函数中最里层 的for
、switch
、select
语句的执行。
如果break
后面有一个标签,那么该标签必须是一个封闭的for
、switch
、select
语句的标签,并且该标签对应的语句的执行会被终止。
go
package main
import "fmt"
func main() {
const n, m = 2, 3
var a [n][m]interface{}
for i := 0; i < n; i++ {
for j := 0; j < m; j++ {
a[i][j] = false
}
}
a[1][1] = true
a[1][2] = nil
// 执行下面两句会打印"报错"字符串
// a[1][2] = true
// a[1][1] = nil
var state string
OuterLoop:
for i := 0; i < n; i++ {
for j := 0; j < m; j++ {
switch a[i][j] {
case true:
state = "存在"
break OuterLoop
case nil:
state = "报错"
break OuterLoop
}
}
}
fmt.Println(state)
}
continue语句
continue
语句通过推进控制到循环的末尾,开始最里层 的封闭for
循环的下一个迭代。for
循环必须在同一个函数中。
如果存在标签,标签必须是一个封闭的for
语句的标签,会对标签对应的语句执行推进。
go
package main
import "fmt"
func main() {
const n, m = 3, 5
var a [n][m]interface{}
for i := 0; i < n; i++ {
for j := 0; j < m; j++ {
a[i][j] = 1
}
}
a[0][2] = "停止此行的计算"
a[1][3] = "停止此行的计算"
a[2][4] = "停止此行的计算"
var sum int
OuterLoop:
for i := 0; i < n; i++ {
for j := 0; j < m; j++ {
if a[i][j] == "停止此行的计算" {
continue OuterLoop
}
sum += (a[i][j]).(int) // 对接口类型进行类型断言
}
}
fmt.Println(sum) // 9
}
goto语句
goto
语句将控制转移到相同函数中的对应标签的语句。
go
goto Error
一个块外部的goto
语句不能跳转到块内部的标签中。
go
if n%2 == 1 {
goto L1
}
for n > 0 {
f()
n--
L1:
f()
n--
}
上面的代码是错误的,因为L1
标签在for
语句的块中,但是goto
不在for
语句的块中。
简单示例:
go
package main
import "fmt"
func main() {
n := 3
if n%2 == 1 {
goto L1
}
return
L1:
f()
n--
}
func f() {
fmt.Println("aaa")
}
defer语句
defer
执行一个函数,该函数的执行被延迟到外围函数返回的时候。不论是正常返回,还是发生错误了返回,defer
注册的函数都会执行。并且先注册的函数后执行。
go
lock(l)
defer unlock(l) // unlocking happens before surrounding function returns
// prints 3 2 1 0 before surrounding function returns
for i := 0; i <= 3; i++ {
defer fmt.Print(i)
}
// f returns 42
func f() (result int) {
defer func() {
// result is accessed after it was set to 6 by the return statement
result *= 7
}()
return 6
}
简单示例:
go
package main
import "fmt"
func main() {
fmt.Println(f()) // 10 / 2 + 3
}
func f() (result int) {
defer func() {
result += 3
}()
defer func() {
result /= 2
}()
return 10
}