Go入门

本文是看了Go官方文档后进行的练习和总结。

Go富有表现力、简洁、干净并且高效 。它的并发机制 让我们能容易地编写充分利用多核和联网机器的程序,它的类型系统 可以实现灵活和模块化的程序构建。Go可以快速编译为机器代码 ,并且还有垃圾回收 的便利性以及运行时反射 的强大功能。它是一种快速、静态类型的编译语言,就像是一种动态类型的解释型语言。

富有表现力在于代码很直观,比如:

go 复制代码
time.Sleep(10 * time.Second)

很直观地知道我是要睡眠10秒。

简洁:包名比如fmt等都很简单直接。

干净:比如引入不使用的包,在go中认为是一种错误,保存代码时会自动删掉引入却没有使用的import语句。

高效:自带并发编程,充分利用多核。

安装

  1. 点击下载地址中的下载按钮下载安装包。

  2. 双击下载好的安装包,根据提示进行安装。

    这个安装包会把Go发行版安装在/usr/local/go这个文件中,并且设置/usr/local/go/bin路径到PATH环境变量中。需要重启终端会话来让这个改变生效。

  3. 验证是否已经安装好Go了:

    shell 复制代码
    $ go version
    go version go1.20.5 darwin/amd64

创建模块

  1. 创建一个文件夹并cd到文件目录下:

    go 复制代码
    $ mkdir practice && cd practice
  2. 初始化模块

    使用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文件的内容是:

    shell 复制代码
    module example.com/practice
    
    go 1.20
  3. 创建一个属于main包的文件practice.go,当运行main包的时候,就会默认执行其中的main函数。

    go 复制代码
    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 复制代码
    $ go run .
    Hello

    (执行go help 可以查看Go的指令列表。)

  4. 导入外部的包

    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
    }

    在代码中导入了外部的包,但是此时这个包并不存在于我们自己的项目代码中。执行:

    shell 复制代码
    $ go mod tidy

    会找到并且下载"github.com/gin-gonic/gin"模块,默认情况下,它会下载最新版本。

    Go会添加gin模块作为一个依赖,并且新增了一个go.sum文件用来验证模块。

    验证模块

    当go命令下载模块的时候,会计算加密哈希并将其与已知值进行比较,以验证文件自首次下载以来没有更改。如果下载的文件没有正确的哈希值,go命令会报告安全错误。(防止下载的包以及缓存的包被恶意篡改)

  5. 还可以执行go mod vendor 指令将模块中包含的依赖复制到项目的vendor文件夹中。

注释

注释作为程序的文档使用。有两种格式的注释:

  • 行内注释:从//开始,在行末结束。
  • 普通注释:从/*开始,以接下来遇到的第一个*/作为结束。

注释不能在runestring字面量中,注释不能包含注释。一个不包含换行符的普通注释就像空白。其他注释就像换行符。

标识符

标志符命名程序的实体,比如变量或者类型。一个标志符由一个或者多个字母和数字组成,标识符的第一个位置必须是一个字母。

下面这四个都是有效的标识符。

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类型的别名,可以理解为单个的字符。

常量可以显式地给到一个类型,或者隐式地给到一个类型。未给到类型的常量的默认类型可能是boolruneintfloat64complex128string中的一个。

在常量声明中,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 = "写点啥"

byteuint8的别名,runeint32的别名。

复合数据类型

复合数据类型就是基础数据类型的组合。

复合数据类型有数组、切片、结构体、指针、函数、接口、映射以及通道类型。

数组

数组是由单一类型元素组成的编号序列。

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 方法得到数组的长度,数组的长度是类型的一部分,是固定不变的。

通过0len(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方法得到切片中元素的个数(切片的长度),与数组不同,切片的长度在执行的过程中可能会发生改变。

通过0len(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包含嵌入字段TS*S的方法集都包含接收者为T的提升方法。*S的方法集还包含接收者为*T的提升方法。
  • 如果S包含嵌入字段*TS*S的方法集都会包含接收者为T*T的提升方法。

简单来说就是:

  • 把类型T嵌入结构体类型S之后,类型*S会包含T*T的方法,类型S只有T的方法。

  • 把类型*T嵌入结构体类型S之后,类型*SS都会包含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中的嵌套接口ET的类型集合就是实现了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 ;; {
    //...
}

标签语句

标签语句就是在语句前面加上标签名称和冒号,标签语句可以作为gotobreak或者continue语句的目标。

go 复制代码
Error: log.Panic("error encountered")

上面这个语句就打上了名为Error的标签。

(具体的使用看后面的gotobreakcontinue部分)

表达式语句

表达式语句就是表达式。有一元表达式或者二元表达式。

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 caseselect语句会永远阻塞。

简单示例:

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语句终止同一个函数中最里层forswitchselect语句的执行。

如果break后面有一个标签,那么该标签必须是一个封闭的forswitchselect语句的标签,并且该标签对应的语句的执行会被终止。

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
}
相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
2401_857610034 小时前
多维视角下的知识管理:Spring Boot应用
java·spring boot·后端
代码小鑫4 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计
颜淡慕潇6 小时前
【K8S问题系列 | 9】如何监控集群CPU使用率并设置告警?
后端·云原生·容器·kubernetes·问题解决
独泪了无痕6 小时前
WebStorm 如何调试 Vue 项目
后端·webstorm
怒放吧德德7 小时前
JUC从实战到源码:JMM总得认识一下吧
java·jvm·后端
代码小鑫8 小时前
A025-基于SpringBoot的售楼管理系统的设计与实现
java·开发语言·spring boot·后端·毕业设计
前端SkyRain8 小时前
后端SpringBoot学习项目-项目基础搭建
spring boot·后端·学习
梦想画家8 小时前
理解Rust 生命周期、所有权和借用机制
开发语言·后端·rust
编程乐趣8 小时前
推荐一个.NetCore开源的CMS项目,功能强大、扩展性强、支持插件的系统!
后端