本文是看了Go官方文档后进行的练习和总结。
Go赋有表现力、简练、洁净并且高效。它的并发机制让我们能容易地编写充分利用多核和联网机器的程序,它的类型系统可以完结灵活和模块化的程序构建。Go可以快速编译为机器代码,并且还有废物回收的便利性以及工作时反射的健壮功用。它是一种快速、静态类型的编译言语,就像是一种动态类型的解释型言语。
赋有表现力在于代码很直观,比如:
time.Sleep(10 * time.Second)
很直观地知道我是要睡眠10秒。
简练:包名比如fmt等都很简略直接。
洁净:比如引入不运用的包,在go中认为是一种差错,保存代码时会主动删掉引入却没有运用的import语句。
高效:自带并发编程,充分利用多核。
设备
-
点击下载地址中的下载按钮下载设备包。
-
双击下载好的设备包,根据提示进行设备。
这个设备包会把Go发行版设备在
/usr/local/go
这个文件中,并且设置/usr/local/go/bin
途径到PATH
环境变量中。需求重启终端会话来让这个改动收效。 -
验证是否现已设备好Go了:
$ go version go version go1.20.5 darwin/amd64
创建模块
-
创建一个文件夹并cd到文件目录下:
$ mkdir practice && cd practice
-
初始化模块
运用go mod init 模块称谓初始化模块,这会创建一个
go.mod
文件,这个文件可以用于对代码进行依托寻找,模块称谓就是模块的途径。当模块中导入了其他模块中的包时,通过go.mod
文件寻找供应这些包的模块。实际开发中,模块途径通常是源码地址的库房方位。例如:
github.com/gin-gonic/gin
。$ go mod init example.com/practice go: creating new go.mod: module example.com/practice
此时
go.mod
文件的内容是:module example.com/practice go 1.20
-
创建一个归于
main
包的文件practice.go
,当工作main
包的时分,就会默许实行其中的main
函数。package main // 用package关键字声明名为main的包 import "fmt" // 用import关键字导入fmt包 func main() { fmt.Println("Hello") // 打印字符串Hello }
-
package
(包)是组织函数的方法,包由同一个目录下的全部文件组成。 -
fmt
包含格式化文本并打印到控制台的函数。fmt包是规范库包中的一个,设备Go时自带的包。 -
当工作
main包
的时分,包中的main函数
会默许实行。
在
main
包地址的文件目录下工作go run . 标明编译和工作其时目录下的main
包。$ go run . Hello
(实行
go help
可以检查Go的指令列表。) -
-
导入外部的包
package 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 }
在代码中导入了外部的包,但是此时这个包并不存在于我们自己的项目代码中。实行:
$ go mod tidy
会找到并且下载
"github.com/gin-gonic/gin"
模块,默许情况下,它会下载最新版本。Go会增加
gin
模块作为一个依托,并且新增了一个go.sum
文件用来验证模块。当go命令下载模块的时分,会核算加密哈希并将其与已知值进行比较,以验证文件自初次下载以来没有更改。假设下载的文件没有正确的哈希值,go命令会陈述安全差错。(避免下载的包以及缓存的包被歹意篡改)
-
还可以实行
go mod vendor
指令将模块中包含的依托复制到项目的vendor
文件夹中。
注释
注释作为程序的文档运用。有两种格式的注释:
- 行内注释:从
//
初步,内行末完毕。 - 一般注释:从
/*
初步,以接下来遇到的第一个*/
作为完毕。
注释不能在rune
和string
字面量中,注释不能包含注释。一个不包含换行符的一般注释就像空白。其他注释就像换行符。
标识符
标志符命名程序的实体,比如变量或许类型。一个标志符由一个或许多个字母和数字组成,标识符的第一个方位有必要是一个字母。
下面这四个都是有用的标识符。
a
_x9
ThisVariableIsExported
终究一个标志符看起来有点乖僻用的是阿拉伯符号,因为在Go中letter(字母),指的是分类为字母分类的Unicode代码点,和下划线符号("_"
),不仅仅是26个英文字母。
所以可以写出下面这样的代码(一般不这样写):
package main
import "fmt"
func main() {
初步吃饭 := true
if 初步吃饭 {
吃米饭("小明")
}
}
func 吃米饭(名字 string) {
fmt.Println(名字 + "吃米饭")
}
关键字
下面的关键字被保留了,不能用作标识符。也就是说在我们为变量、函数、类型等命名的时分,不能运用下面这些单词。
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初步。
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
}
变量
变量是一个值的存储方位。能存储什么值取决于变量的类型。
假设仅仅声明没有赋值,每个变量会被初始化为类型的零值。
变量的静态类型是声明时给到的那个类型。接口类型的变量还有一个动态类型,是工作时赋给变量的值的类型。
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
}
运用:=
可以把声明和赋值放在同一行:
a := 1
等同于:
var a int
a = 1
底子数据类型
底子数据类型有布尔类型、数字类型(整数类型、浮点数类型、复数类型)、字符串类型。
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
的别号。
复合数据类型
复合数据类型就是基础数据类型的组合。
复合数据类型有数组、切片、结构体、指针、函数、接口、映射以及通道类型。
数组
数组是由单一类型元素组成的编号序列。
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
的整数索引,可以找到数组对应方位的元素。
未初始化的数组中的每个元素是元素类型的零值。
切片
切片是对一个底层数组的连续片段的描述,一同供应对底层数组的元素的编号序列的拜访。
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
方法初始化切片:
make([]T, length, capacity)
make总是会分配一个新的底层数组给切片。以下两个表达式是相同的:
make([]int, 50, 100)
new([100]int)[0:50]
内置的函数new(T)
会在工作时为类型T
的变量分配内存,并且回来指向该变量的类型为*T
的值。
make(T)
会回来类型为T
的值。并且T
的核心类型只能是切片、映射或通道。
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]
结构体
结构体是由命名元素(称为字段)组成的序列,每个字段有一个名字和一个类型。
// An empty struct.
struct {}
// A struct with 6 fields.
struct {
x, y int
u float32
_ float32 // padding
A *[]int
F func()
}
下划线的意思是这个方位有一个字段,但是我们不需求用这个字段,所以就丢弃这个字段。
一个字段声明晰类型,但没有显式的字段称谓,这种字段称为嵌入字段。一个嵌入字段有必要是类型称谓T
,或许指向非接口类型的类型称谓*T
,并且T
自身不是一个指针类型。
// 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
的方法。
(看完后边的函数和方法之后再回过头来看这儿应该就能明白)
进步字段和一般字段的用法相同,除了不能在复合字面量中作为字段称谓运用。
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),它会成为相应的字段声明中全部字段的特色。
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"`
}
未初始化的结构体的字段都是对应类型的零值。
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
。
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
函数
函数类型标明参数和作用类型相同的全部函数的集结。
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
。
在参数和作用列表中,称谓有必要全部呈现或全部省掉。
参数列表和作用列表都有必要在括号中,但有一个破例,当只需一个未命名的回来作用的时分,可以不必括号。
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)
}
}
方法
假设函数有一个接收者,这种函数称为方法。
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
就是接收者,
func (p *Point) Length() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
Length
方法是绑定在*Point
类型上的,但是我们在运用的时分直接运用了:
p := Point{
x: 3,
y: 4,
}
fmt.Println(p.Length()) // 5
p的类型是Point
而不是*Point
,也能正常调用Length
方法,这是因为Go为了便利用户运用,在背面作了转化,会将Point
类型转化为*Point
类型,然后调用方法。假设一个方法Point
是接收者,那用*Point
类型去调用该方法的时分,也会在背面将*Point
类型转化为Point
类型来调用Point
类型的方法。
假设接收者的底子类型是泛型,接收者有必要为方法声明对应的类型参数。
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
假设类型定义中声明晰类型参数,那么类型称谓就标明一个泛型。泛型会在运用时被实例化。
泛型类似函数的形参和实参,定义的时分定义形参类型,运用的时分传入实参类型。
type List[T any] struct {
value T
next *List[T]
}
示例1:
当不运用泛型的时分,关于如下代码,
type Point struct {
x, y float64
}
func (p *Point) Length() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
假设要将float64
类型改为float32
类型,那么就需求重新写一个类型和函数:
type Point struct {
x, y float32
}
func (p *Point) Length() float32 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
假设运用泛型,则可以直接这样写:
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:
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
}
一些其他泛型示例:
类型形参列表 类型实参 类型实参替换类型形参之后
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
。
底子接口
完全由方法列表定义类型集结的接口称为底子接口。
// A simple File interface.
interface {
Read([]byte) (int, error)
Write([]byte) (int, error)
Close() error
}
无论什么类型,只需包含上述接口中的三个方法,就是完结了该接口。
全部类型都完结了空接口类型interface{}
,空接口的别号是预声明类型any
。
type Locker interface {
Lock()
Unlock()
}
func (p T) Lock() { … }
func (p T) Unlock() { … }
类型T
完结了接口Locker
。
简略示例:
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
中声明的方法的全部类型。
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
}
简略示例:
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
。
// 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
就是类型捆绑,捆绑类型参数只能是可比较的类型。
简略示例:
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
。
键类型有必要能进行比较操作(==
和!=
),所以函数、映射和切片不能作为键。
// 创建一个空的映射
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)
}
删去一个不存在的键不会报错,获取一个不存在的键会获得值对应的类型的零值:
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 Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)
channel
通道给并行实行的函数供应了一种通讯的方法,通过发送和接收某个特定类型的值进行通讯。
未初始化的通道的值是nil
。
可选的<-
操作符指定通道的方向,发送或接收。假设给到一个方向,通道就是定向的,不然就是双向的。通过赋值或许显式转化,一个通道可以被捆绑为只发送或许只接收。
这儿的发送和接收是面对通道的,向通道发送就是发送,从通道里接收就是接收。而不是通道接收数据算作接收。
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
函数初始化通道。
make(chan int, 100)
第二个参数是可选的,标明容量,容量设置的是通道的缓存大小。假设容量为0或许没有第二个参数,就标明是无缓存通道。不然这个通道就是缓存通道。
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
进行封闭。
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
函数的部分可以替换为:
func consumer(c chan int) {
ele, ok := <-c
for ok {
fmt.Println("从通道中获得得值", ele)
ele, ok = <-c
}
}
语句
空语句
空语句是一个空的方位,什么也不做。比如下面的for语句中两个冒号周围的3个方位,就是空语句,什么也没有,什么也不做。
for ;; {
//...
}
标签语句
标签语句就是在语句前面加上标签称谓和冒号,标签语句可以作为goto
、break
或许continue
语句的方针。
Error: log.Panic("error encountered")
上面这个语句就打上了名为Error的标签。
(详细的运用看后边的goto
、break
、continue
部分)
表达式语句
表达式语句就是表达式。有一元表达式或许二元表达式。
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)包含在一元表达式中,示例如下:
x
2
(s + ".txt")
f(3.1415, true)
Point{1, 2}
m["foo"]
s[i : j + 1]
obj.color
f.p[i].x()
简略示例:
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("%bn", x) // 1
fmt.Printf("%bn", ^x) // 11111110
}
if语句
if
语句根据一个布尔表达式的值抉择两个分支的有条件实行。假设布尔表达式的值为真,会实行if
分支,不然实行else
分支。
if x > max {
x = max
}
布尔表达式的前面还可以写一个简略的语句,会在剖析布尔表达式前实行:
if x := f(); x < y {
return x
} else if x > z {
return z
} else {
return y
}
switch语句
表达式switch
case
包含的表达式与switch
包含的表达式比较。忽略switch
表达式等同于布尔值true
。
运用fallthrough
将控制权转移给下一个case
。
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
表达式中的类型比较。
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
子句控制。
单个条件:
for a < b {
a *= 2
}
只需a < b
一贯成立,a *= 2
就会一贯实行。
for子句:
for i := 0; i < 10; i++ {
f(i)
}
i := 0
是初次迭代前只实行一次的初始化语句;i < 10
是条件语句;i++
是每次代码实行完毕之后实行的语句。
初始化语句(init statement),条件,后续语句(post statement)都可以省掉。
for cond { S() } is the same as for ; cond ; { S() }
for { S() } is the same as for true { S() }
包含range子句的for语句:
规划表达式 回来的第一个值 回来的第二个值
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
简略示例:
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 %vn", 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 Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)
select语句
select
语句挑选哪个或许的发送或接收操作会被处理。它和switch
语句看起来有点像,但是全部的case
都是指的通讯操作。
因为nil
通道上的通讯永久不会继续,只需nil
通道,没有default case
的select
语句会永久阻塞。
简略示例:
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
}
其他比如:
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
回来前实行。
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
语句进行核算。
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
后边就不能什么都不加:
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
语句的标签,并且该标签对应的语句的实行会被间断。
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
语句的标签,会对标签对应的语句实行推动。
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
语句将控制转移到相同函数中的对应标签的语句。
goto Error
一个块外部的goto
语句不能跳转到块内部的标签中。
if n%2 == 1 {
goto L1
}
for n > 0 {
f()
n--
L1:
f()
n--
}
上面的代码是差错的,因为L1
标签在for
语句的块中,但是goto
不在for
语句的块中。
简略示例:
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
注册的函数都会实行。并且先注册的函数后实行。
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
}
简略示例:
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
}