学习 Go:完整课程目录 Go 是什么?为什么要学习 Go?安装和设置 Hello World 变量和数据类型 字符串格式化 流程控制函数 模块 工作区 包 实用命令 构建指针 结构体 方法 数组和切片 映射 接口 错误 恐慌和恢复 测试 泛型 并发 Goroutines 通道 选择 WaitGroups 互斥体 后续步骤

2025-06-11

学习 Go:完整课程

目录

什么是 Go?

为什么要学习 Go?

安装和设置

你好世界

变量和数据类型

字符串格式化

流量控制

功能

模块

工作区

套餐

有用的命令

建造

指针

结构体

方法

数组和切片

地图

接口

错误

恐慌与恢复

测试

泛型

并发

Goroutines

频道

选择

等待组

互斥体

后续步骤

欢迎来到本课程,感谢你学习 Go。希望本课程能为你带来精彩的学习体验!

注意:本课程也可在我的网站上免费获取。

目录

什么是 Go?

Go(也称为Golang)是 Google 于 2007 年开发并于 2009 年开源的一种编程语言。

它注重简洁、可靠和高效。它旨在将静态类型和编译语言的高效、快速和安全性与动态语言的易编程性相结合,让编程再次变得更有趣。

在某种程度上,他们希望结合 Python 和 C++ 的优点,以便能够构建能够利用多核处理器的可靠系统。

为什么要学习 Go?

在开始本课程之前,我们先来谈谈为什么要学习 Go

1. 易于学习

易于学习.png

Go 相当容易学习,并且拥有一个支持性和活跃的社区。

作为一种多用途语言,您可以将它用于后端开发、云计算以及最近的数据科学等领域。

2.快速可靠

快速可靠.png

这使得它非常适合分布式系统。Kubernetes 和 Docker 等项目都是用 Go 编写的。

3. 简单但强大

简单而强大.png

Go 语言只有 25 个关键字,这使得它易于阅读、编写和维护。语言本身非常简洁。

但不要被它的简单性所迷惑,Go 有几个强大的功能,我们稍后会在课程中学习。

4. 职业机会

职业机会.png

Go 发展迅速,各种规模的公司都在采用它。同时,它也带来了新的高薪工作机会。

希望这能让你对 Go 产生兴趣。让我们开始这门课程吧。

在本教程中,我们将安装 Go 并设置我们的代码编辑器

安装和设置

下载

我们可以从下载部分安装 Go。

下载

安装

这些说明来自官方网站

MacOS

  1. 打开下载的软件包文件,然后按照提示安装 Go。
    软件包会将 Go 发行版安装到/usr/local/go。软件包会将/usr/local/go/bin目录添加到你的PATH环境变量中。
    你可能需要重启所有打开的终端会话才能使更改生效。

  2. 打开命令提示符并输入以下命令来验证是否已安装 Go:

$ go version
Enter fullscreen mode Exit fullscreen mode
  1. 确认该命令打印已安装的 Go 版本。

Linux

  1. 通过删除文件夹(如果存在)来删除所有以前的 Go 安装/usr/local/go,然后将刚下载的档案提取到中/usr/local,在中创建一个新的 Go 树/usr/local/go
$ rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.1.linux-amd64.tar.gz
Enter fullscreen mode Exit fullscreen mode

(您可能需要以 root 身份或通过 sudo 运行该命令)

不要将存档解压到现有目录/usr/local/go树中。这会导致 Go 安装失败。

  1. 添加/usr/local/go/bin到 PATH 环境变量。你可以将以下行添加到你的$HOME/.profile/etc/profile(对于系统范围的安装)中:
export PATH=$PATH:/usr/local/go/bin
Enter fullscreen mode Exit fullscreen mode

注意:对配置文件所做的更改可能要等到下次登录计算机时才会生效。要立即应用更改,只需直接运行 shell 命令,或使用 source 等命令从配置文件中执行更改$HOME/.profile

  1. 打开命令提示符并输入以下命令来验证是否已安装 Go:
$ go version
Enter fullscreen mode Exit fullscreen mode
  1. 确认该命令打印已安装的 Go 版本。

视窗

  1. 打开您下载的 MSI 文件并按照提示安装 Go。

默认情况下,安装程序将安装“转到 Program Files”或“Program Files (x86)”。
您可以根据需要更改位置。安装完成后,您需要关闭并重新打开所有打开的命令提示符,以便安装程序对环境所做的更改能够反映在命令提示符中。

  1. 验证您是否已安装 Go。
    1. 在 Windows 中,单击“开始”菜单。
    2. 在菜单的搜索框中,输入 cmd,然后按 Enter 键。
    3. 在出现的命令提示符窗口中,键入以下命令:
$ go version
Enter fullscreen mode Exit fullscreen mode
  1. 确认该命令打印已安装的 Go 版本。

VS 代码

在本课程中,我将使用VS Code ,您可以从这里下载它

vscode

随意使用您喜欢的任何其他代码编辑器

扩大

确保还安装了Go 扩展
,这使得 在 VS Code 中使用 Go 更加容易。

扩大

这就是 Go 的安装和设置,让我们开始课程并编写我们的第一个 hello world!

你好世界

让我们编写第一个 Hello World 程序,首先初始化一个模块。为此,我们可以使用以下go mod命令

$ go mod init example
Enter fullscreen mode Exit fullscreen mode

可是等等……什么是 a module?别担心,我们很快会讨论这个问题!不过现在,先假设模块基本上是 Go 包的集合。

接下来,让我们创建一个main.go文件并编写一个简单打印 hello world 的程序。

package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}
Enter fullscreen mode Exit fullscreen mode

如果你想知道,fmt它是 Go 标准库的一部分,它是 Go 语言提供的一组核心包

现在,让我们快速分解一下我们在这里所做的事情,或者更确切地说,分解一下 Go 程序的结构

首先,我们定义一个包,例如main

package main
Enter fullscreen mode Exit fullscreen mode

然后,我们有一些进口

import "fmt"
Enter fullscreen mode Exit fullscreen mode

最后但并非最不重要的是我们的main函数,它充当我们应用程序的入口点,就像 C、Java 或 C# 等其他语言一样。

func main() {
    ...
}
Enter fullscreen mode Exit fullscreen mode

请记住,这里的目标是记住这一点,在课程的后面,我们将详细了解、和其他内容functionsimports

最后,要运行我们的代码,我们可以简单地使用go run命令

$ go run main.go
Hello World!
Enter fullscreen mode Exit fullscreen mode

恭喜,您刚刚编写了您的第一个 Go 程序!

让我们继续下一个话题

变量和数据类型

在本教程中,我们将学习变量。我们还将学习 Go 提供的不同数据类型。

变量

让我们从声明一个变量开始

这也称为无初始化声明

var foo string
Enter fullscreen mode Exit fullscreen mode

带有初始化的声明

var foo string = "Go is awesome"
Enter fullscreen mode Exit fullscreen mode

多个声明

var foo, bar string = "Hello", "World"
// OR
var (
    foo string = "Hello"
    bar string  = "World"
)
Enter fullscreen mode Exit fullscreen mode

类型被省略但会被推断

var foo = "What's my type?"
Enter fullscreen mode Exit fullscreen mode

简写,这里我们省略了var关键字,类型始终是隐式的。这就是我们大多数情况下变量声明的方式。我们也使用:=for 声明加赋值

foo := "Shorthand!"
Enter fullscreen mode Exit fullscreen mode

注意:简写仅适用于function主体内部

常量

我们也可以用关键字声明常量const。顾名思义,常量是固定值,不能重新赋值。

const constant = "This is a constant"
Enter fullscreen mode Exit fullscreen mode

数据类型

太棒了!现在我们来看看 Go 中一些基本的数据类型。从字符串开始。

细绳

在 Go 中,字符串是字节序列。

它们使用双引号或反引号声明,可以跨越多行

var name string = "My name is Go"

var bio string = `I am statically typed.
                                    I was designed at Google.`
Enter fullscreen mode Exit fullscreen mode

布尔值

接下来是bool用于存储布尔值的 。它可以有两个可能的值 -truefalse

var value bool = false
var isItTrue bool = true
Enter fullscreen mode Exit fullscreen mode

运算符

我们可以在布尔类型上使用以下运算符

逻辑 &&
平等 == !=

数字类型

现在我们来讨论一下数字类型,首先是

有符号整数和无符号整数

Go 有几种不同大小的内置整数类型,用于存储有符号和无符号整数

int泛型和类型的大小uint取决于平台。这意味着在 32 位系统上,它的宽度为 32 位;在 64 位系统上,它的宽度为 64 位。

var i int = 404                     // Platform dependent
var i8 int8 = 127                   // -128 to 127
var i16 int16 = 32767               // -2^15 to 2^15 - 1
var i32 int32 = -2147483647         // -2^31 to 2^31 - 1
var i64 int64 = 9223372036854775807 // -2^63 to 2^63 - 1
Enter fullscreen mode Exit fullscreen mode

与有符号整数类似,我们有无符号整数。

var ui uint = 404                     // Platform dependent
var ui8 uint8 = 255                   // 0 to 255
var ui16 uint16 = 65535               // 0 to 2^16
var ui32 uint32 = 2147483647          // 0 to 2^32
var ui64 uint64 = 9223372036854775807 // 0 to 2^64
var uiptr uintptr                     // Integer representation of a memory address
Enter fullscreen mode Exit fullscreen mode

如果你注意到的话,还有一个无符号整数指针uintptr类型,它是内存地址的整数表示。我们不建议使用它,所以我们不必担心。

那么我们应该使用哪一个呢?

建议无论何时我们需要一个整数值,都应该使用,int除非我们有特殊原因要使用有大小或无符号的整数类型。

整数别名类型

接下来,让我们讨论整数别名类型。

字节和符文

Golang 有两个额外的整数类型,称为byte和,它们分别是和数据类型的rune别名uint8int32

type byte = uint8
type rune = int32
Enter fullscreen mode Exit fullscreen mode

Arune代表一个 unicode 代码点。

var b byte = 'a'
var r rune = '🍕'
Enter fullscreen mode Exit fullscreen mode

浮点

接下来,我们有浮点类型,用于存储带有小数部分的数字。

Go 有两种浮点类型float32float64。两种类型都遵循 IEEE-754 标准。

浮点值的默认类型是 float64

var f32 float32 = 1.7812 // IEEE-754 32-bit
var f64 float64 = 3.1415 // IEEE-754 64-bit
Enter fullscreen mode Exit fullscreen mode

运算符

Go 提供了几个对数字类型执行运算的运算符。

数字运算符

复杂的

Go 中有 2 种复数类型。complex128 的实部和虚部都是 float64,complex64 的实部和虚部都是 float32。

我们可以使用内置复数函数或文字来定义复数。

var c1 complex128 = complex(10, 1)
var c2 complex64 = 12 + 4i
Enter fullscreen mode Exit fullscreen mode

零值

现在我们来讨论一下零值。在 Go 中,任何声明时没有明确指定初始值的变量都会被赋予零值。例如,我们声明一些变量,并看看

var i int
var f float64
var b bool
var s string

fmt.Printf("%v %v %v %q\n", i, f, b, s)
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
0 0 false ""
Enter fullscreen mode Exit fullscreen mode

因此,正如我们所见int,和float被赋值为 0、boolfalse 和string空字符串。这与其他语言的做法截然不同。例如,大多数语言将未赋值的变量初始化为 null 或 undefined。

这很棒,但是我们的函数中的百分号是什么呢Printf?正如你已经猜到的,它们用于格式化,我们将在下一个教程中学习它们。

类型转换

接下来,我们已经了解了数据类型的工作原理,让我们看看如何进行类型转换。

i := 42
f := float64(i)
u := uint(f)

fmt.Printf("%T %T", f, u)
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
float64 uint
Enter fullscreen mode Exit fullscreen mode

我们可以看到,它将类型打印为float64uint

请注意,这与解析不同

别名类型

Go 1.9 中引入了别名类型

它们允许开发人员为现有类型提供备用名称,并将其与底层类型互换使用。

package main

import "fmt"

type MyAlias = string

func main() {
    var str MyAlias = "I am an alias"

    fmt.Printf("%T - %s", str, str) // Output: string - I am an alias
}
Enter fullscreen mode Exit fullscreen mode

定义类型

最后,我们定义了与别名类型不同不使用等号的类型。

package main

import "fmt"

type MyDefined string

func main() {
    var str MyDefined = "I am defined"

    fmt.Printf("%T - %s", str, str) // Output: main.MyDefined - I am defined
}
Enter fullscreen mode Exit fullscreen mode

但是等等...有什么区别?

因此,定义类型不仅仅是为类型命名。

它首先定义了一个具有底层类型的新命名类型。然而,这个定义的类型与任何其他类型(包括其底层类型)都不同。

因此,它不能与别名类型等底层类型互换使用。

一开始有点令人困惑,希望这个例子能够让事情变得清楚。

package main

import "fmt"

type MyAlias = string

type MyDefined string

func main() {
    var alias MyAlias
    var def MyDefined

    // ✅ Works
    var copy1 string = alias

    // ❌ Cannot use str (variable of type MyDefined) as string value in variable
    var copy2 string = def

    fmt.Println(copy1, copy2)
}
Enter fullscreen mode Exit fullscreen mode

我们可以看到,与别名类型不同,我们不能将定义的类型与底层类型互换使用。

好了,Go 中的变量和数据类型基本就这些了。下期再见。

字符串格式化

在本教程中,我们将学习字符串格式化,有时也称为模板。

fmt包中包含许多函数。为了节省时间,我们将讨论最常用的函数。让我们fmt.Print从 main 函数开始。

...

fmt.Print("What", "is", "your", "name?")
fmt.Print("My", "name", "is", "golang")
...
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Whatisyourname?Mynameisgolang
Enter fullscreen mode Exit fullscreen mode

正如我们所见,Print它不格式化任何内容,只是获取一个字符串并打印它

接下来,我们有Println相同的Print,但它在末尾添加了一个新行,并在参数之间插入空格

...

fmt.Println("What", "is", "your", "name?")
fmt.Println("My", "name", "is", "golang")
...
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
What is your name?
My name is golang
Enter fullscreen mode Exit fullscreen mode

这好多了!

接下来,我们Printf还有所谓的“打印格式化程序”,它允许我们格式化数字、字符串、布尔值等等。

让我们看一个例子

...
name := "golang"

fmt.Println("What is your name?")
fmt.Printf("My name is %s", name)
...
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
What is your name?
My name is golang
Enter fullscreen mode Exit fullscreen mode

我们可以看到,它%s被我们的变量替换了name

但问题是它是什么%s以及它意味着什么?

这些被称为注解动词,它们告诉函数如何格式化参数。我们可以用它们来控制宽度、类型和精度等参数,而且有很多。这里有一个速查表

现在我们快速看一些例子。这里我们将尝试计算一个百分比并将其打印到控制台。

...
percent := (3/5) * 100
fmt.Printf("%f", percent)
...
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
58.181818
Enter fullscreen mode Exit fullscreen mode

假设我们想要的只是58.182 点精度,我们也可以使用.2f

此外,要添加实际的百分号,我们需要对其进行转义。

...
percent := (3/5) * 100
fmt.Printf("%.2f %%", percent)
...
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
58.18 %
Enter fullscreen mode Exit fullscreen mode

这将引出SprintSprintlnSprintf。它们与打印函数基本相同,唯一的区别是它们返回字符串而不是打印它。

让我们举个例子

...
s := fmt.Sprintf("hex:%x bin:%b", 10 ,10)
fmt.Println(s)
...
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
hex:a bin:1010
Enter fullscreen mode Exit fullscreen mode

因此,正如我们所看到的,Sprintf我们的整数格式化为十六进制或二进制,并将其作为字符串返回。

最后,我们有多行字符串文字,可以像这样使用

...
msg := `
Hello from
multiline
`

fmt.Println(msg)
...
Enter fullscreen mode Exit fullscreen mode

太棒了!但这只是冰山一角……所以请务必查看 Go 文档中的fmt软件包。

对于有 C/C++ 背景的人来说,这应该很自然。但如果你之前学习过 Python 或 JavaScript,一开始可能会有点奇怪。但它非常强大,你会发现这个功能被广泛使用。

流量控制

让我们讨论一下流程控制,从 if/else 开始。

如果/否则

这与你预期的几乎相同,但表达式不需要用括号括起来()

func main() {
    x := 10

    if x > 5 {
        fmt.Println("x is gt 5")
    } else if x > 10 {
        fmt.Println("x is gt 10")
    } else {
        fmt.Println("else case")
    }
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
x is gt 5
Enter fullscreen mode Exit fullscreen mode

紧凑型

我们还可以压缩 if 语句

func main() {
    if x := 10; x > 5 {
        fmt.Println("x is gt 5")
    }
}
Enter fullscreen mode Exit fullscreen mode

注意:这种模式很常见

转变

接下来,我们有switch语句,这通常是编写条件逻辑的一种更简短的方式。

在 Go 中,switch case 只会执行第一个值等于条件表达式的 case,而不会执行后面的所有 case。因此,与其他语言不同,break每个 case 的末尾都会自动添加 statement。

这意味着它会从上到下评估案例,当一个案例成功时停止。我们来看一个例子

func main() {
    day := "monday"

    switch day {
    case "monday":
        fmt.Println("time to work!")
    case "friday":
        fmt.Println("let's party")
    default:
        fmt.Println("browse memes")
    }
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
time to work!
Enter fullscreen mode Exit fullscreen mode

Switch 还支持像这样的简写声明

    switch day := "monday"; day {
    case "monday":
        fmt.Println("time to work!")
    case "friday":
        fmt.Println("let's party")
    default:
        fmt.Println("browse memes")
    }
Enter fullscreen mode Exit fullscreen mode

我们还可以使用fallthrough关键字将控制权转移到下一个案例,即使当前案例可能已经匹配。

    switch day := "monday"; day {
    case "monday":
        fmt.Println("time to work!")
        fallthrough
    case "friday":
        fmt.Println("let's party")
    default:
        fmt.Println("browse memes")
    }
Enter fullscreen mode Exit fullscreen mode

如果我们运行这个,我们会看到,在第一个 case 匹配之后,switch 语句会继续执行下一个 case,因为fallthrough关键字

$ go run main.go
time to work!
let's party
Enter fullscreen mode Exit fullscreen mode

我们也可以不带任何条件地使用它,这与switch true

x := 10

switch {
    case x > 5:
        fmt.Println("x is greater")
    default:
        fmt.Println("x is not greater")
}
Enter fullscreen mode Exit fullscreen mode

循环

现在,让我们将注意力转向循环。

所以在 Go 中,我们只有一种循环,那就是for循环。

但它的用途非常广泛。与 if 语句一样,for 循环不需要括号,这一点()与其他语言不同。

让我们从基本的 for 循环开始。

func main() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
}
Enter fullscreen mode Exit fullscreen mode

基本for循环由三个用分号分隔的组件组成:

  • init 语句:在第一次迭代之前执行
  • 条件表达式:每次迭代之前进行评估
  • post 语句:在每次迭代结束时执行

中断并继续

正如预期的那样,Go 也支持breakandcontinue语句进行循环控制。我们来尝试一个简单的例子

func main() {
    for i := 0; i < 10; i++ {
        if i < 2 {
            continue
        }

        fmt.Println(i)

        if i > 5 {
            break
    }
    }

    fmt.Println("We broke out!")
}
Enter fullscreen mode Exit fullscreen mode

因此,continue当我们想要跳过循环的剩余部分时使用语句,break当我们想要跳出循环时使用语句。

此外,Init 和 post 语句是可选的,因此我们可以使我们的for循环也像 while 循环一样运行。

func main() {
    i := 0

    for ;i < 10; {
        i += 1
    }
}
Enter fullscreen mode Exit fullscreen mode

注意:我们还可以删除多余的分号,使其更简洁一些

永远循环

最后,如果我们省略循环条件,它将永远循环,因此无限循环可以简洁地表达。这也被称为永久循环

func main() {
    for {
        // do stuff here
    }
}
Enter fullscreen mode Exit fullscreen mode

好吧,关于流量控制就讲到这里,下次再见!

功能

在本教程中,我们将讨论如何在 Go 中使用函数。首先,让我们从一个简单的函数声明开始。

简单声明

func myFunction() {}
Enter fullscreen mode Exit fullscreen mode

我们可以按如下方式调用或执行

...
myFunction()
...
Enter fullscreen mode Exit fullscreen mode

让我们向它传递一些参数

func main() {
    myFunction("Hello")
}

func myFunction(p1 string) {
    fmt.Printtln(p1)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Enter fullscreen mode Exit fullscreen mode

正如我们所见,它打印了我们的消息

如果连续的参数具有相同的类型,我们也可以进行简写声明。例如,

func myNextFunction(p1, p2 string) {}
Enter fullscreen mode Exit fullscreen mode

返回值

现在让我们也返回一个值

func main() {
    s := myFunction("Hello")
    fmt.Println(s)
}

func myFunction(p1 string) string {
    msg := fmt.Sprintf("%s function", p1)
    return msg
}
Enter fullscreen mode Exit fullscreen mode

多次返回

为什么每次只返回一个值呢?Go 还支持多个返回值!

func main() {
    s, i := myFunction("Hello")
    fmt.Println(s, i)
}

func myFunction(p1 string) (string, int) {
    msg := fmt.Sprintf("%s function", p1)
    return msg, 10
}
Enter fullscreen mode Exit fullscreen mode

命名回报

另一个很酷的功能是命名返回,其中返回值可以被命名并视为它们自己的变量

func myFunction(p1 string) (s string, i int) {
    s = fmt.Sprintf("%s function", p1)
    i = 10

    return
}
Enter fullscreen mode Exit fullscreen mode

注意我们如何添加一个return没有任何参数的语句,这也称为裸返回

我想说的是,尽管这个功能很有趣,但请小心使用,因为这可能会降低较大函数的可读性

函数作为值

接下来,我们来谈谈函数作为值的概念。在 Go 中,函数是一等公民,我们可以将其作为值来使用。所以,让我们整理一下函数,然后尝试一下!

func myFunction() {
    fn := func() {
        fmt.Println("inside fn")
    }

    fn()
}
Enter fullscreen mode Exit fullscreen mode

我们还可以通过创建匿名函数fn来简化这一点

func myFunction() {
    func() {
        fmt.Println("inside fn")
    }()
}
Enter fullscreen mode Exit fullscreen mode

注意我们是如何使用括号来执行它的

闭包

为什么要止步于此呢?我们再返回一个函数,这样就创建了一个叫做闭包的东西。一个简单的定义是,闭包是一个引用函数体外部变量的函数值。

闭包具有词法作用域,这意味着函数可以在定义函数时访问作用域内的值。

func myFunction() func(int) int {
    sum := 0

    return func(v int) int {
        sum += v

        return sum
    }
}
Enter fullscreen mode Exit fullscreen mode
...
add := myFunction()

add(5)
fmt.Println(add(10))
...
Enter fullscreen mode Exit fullscreen mode

sum可以看到,由于变量绑定到了函数,结果为 15。这是一个非常强大的概念,绝对值得了解。

可变函数

现在让我们看一下可变函数,它们可以使用...省略号运算符接受零个或多个参数。

这里的一个例子是一个可以添加一堆值的函数

func main() {
    sum := add(1, 2, 3, 5)
    fmt.Println(sum)
}

func add(values ...int) int {
    sum := 0

    for _, v := range values {
        sum += v
    }

    return sum
}
Enter fullscreen mode Exit fullscreen mode

很酷吧?另外,不用担心range关键词,我们稍后会在课程中讨论。

有趣的事实fmt.Println是一个可变函数,这就是我们能够向它传递多个值的方式。

推迟

最后,让我们讨论一下defer关键字,它让我们推迟函数的执行,直到周围的函数返回。

func main() {
    defer fmt.Println("I am finished")
    fmt.Println("Doing some work...")
}
Enter fullscreen mode Exit fullscreen mode

我们可以使用多个 defer 函数吗?当然可以,这就引出了所谓的defer stack,我们来看一个例子

func main() {
    defer fmt.Println("I am finished")
    defer fmt.Prinlnt("Are you?")

    fmt.Println("Doing some work...")
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Doing some work...
Are you?
I am finished
Enter fullscreen mode Exit fullscreen mode

我们可以看到,defer 语句被堆叠起来并以后进先出的方式执行。

因此,Defer 非常有用,通常用于清理或错误处理。

函数也可以与泛型一起使用,但我们将在课程后面讨论它们。

这就是 Go 中的函数,下篇教程再见。

模块

在本教程中,我们将学习模块。

那么什么是模块?

简单来说,模块是存储在文件树中的Go 包go.mod的集合,其根目录位于文件之外 $GOPATH/src

Go 1.11 引入了 Go 模块,该版本提供了对版本和模块的原生支持。之前,GO111MODULE=on当模块功能处于实验阶段时,我们需要使用标志来启用它。但现在,从 Go 1.13 开始,模块模式已成为所有开发环境的默认模式。

但等等,是什么GOPATH

嗯,GOPATH这是一个定义工作区根目录的变量,它包含以下文件夹:

  • src:包含按层次结构组织的 Go 源代码
  • pkg:包含已编译的包代码
  • bin:包含已编译的二进制文件和可执行文件。

gopath

像之前一样,让我们​​使用go mod init命令创建一个新模块,该命令创建一个新模块并初始化go.mod描述它的文件

$ go mod init example
Enter fullscreen mode Exit fullscreen mode

这里需要注意的是,如果你计划发布某个 Go 模块,它也可以对应一个 Github 仓库。例如:

$ go mod init example
Enter fullscreen mode Exit fullscreen mode

现在,让我们探索一下定义模块的模块路径的go.mod文件以及用于根目录的导入路径及其依赖要求

module <name>

go <version>

require (
    ...
)
Enter fullscreen mode Exit fullscreen mode

如果我们想添加新的依赖项,我们将使用go install命令

$ go install github.com/rs/zerolog
Enter fullscreen mode Exit fullscreen mode

我们可以看到,还创建了一个文件。该文件包含新模块内容的go.sum预期哈希值。

go list我们可以使用以下命令列出所有依赖项

$ go list -m all
Enter fullscreen mode Exit fullscreen mode

如果依赖项未使用,我们可以使用go mod tidy命令简单地将其删除。

$ go mod tidy
Enter fullscreen mode Exit fullscreen mode

结束对模块的讨论后,我们来讨论一下供应商。

Vendoring 是指为项目正在使用的第三方软件包制作自己的副本。这些副本通常放置在每个项目中,然后保存在项目仓库中。

这可以通过go mod vendor命令完成

因此,让我们使用以下方法重新安装已删除的模块go mod tidy

package main

import "github.com/rs/zerolog/log"

func main() {
    log.Info().Msg("Hello")
}
Enter fullscreen mode Exit fullscreen mode
$ go mod tidy
go: finding module for package github.com/rs/zerolog/log
go: found github.com/rs/zerolog/log in github.com/rs/zerolog v1.26.1
Enter fullscreen mode Exit fullscreen mode
$ go mod vendor
Enter fullscreen mode Exit fullscreen mode
├── go.mod
├── go.sum
├── go.work
├── main.go
└── vendor
    ├── github.com
    │   └── rs
    │       └── zerolog
    │           └── ...
    └── modules.txt
Enter fullscreen mode Exit fullscreen mode

所以对于模块来说这基本上就是这样,我们将在下一个教程中见到您。

工作区

在本教程中,我们将了解 Go 1.18 中引入的多模块工作区

工作区允许我们同时处理多个模块,而无需go.mod为每个模块编辑文件。在解析依赖关系时,工作区中的每个模块都被视为根模块。

为了更好地理解这一点,我们先创建一个hello模块

$ mkdir workspaces && cd workspaces
$ mkdir hello && cd hello
$ go mod init hello
Enter fullscreen mode Exit fullscreen mode

为了演示目的,我将添加一个简单的main.go并安装一个示例包。

package main

import (
    "fmt"

    "golang.org/x/example/stringutil"
)

func main() {
    result := stringutil.Reverse("Hello Workspace")
    fmt.Println(result)
}
Enter fullscreen mode Exit fullscreen mode
$ go get golang.org/x/example
go: downloading golang.org/x/example v0.0.0-20220412213650-2e68773dfca0
go: added golang.org/x/example v0.0.0-20220412213650-2e68773dfca0
Enter fullscreen mode Exit fullscreen mode

如果我们运行这个,我们应该看到相反的输出。

$ go run main.go
ecapskroW olleH
Enter fullscreen mode Exit fullscreen mode

这很好,但是如果我们想修改stringutil我们的代码所依赖的模块怎么办?

到目前为止,我们必须使用文件replace中的指令来完成此操作go.mod,但现在让我们看看如何在此处使用工作区。

因此,让我们在workspace目录中创建我们的工作区

$ go work init
Enter fullscreen mode Exit fullscreen mode

这将创建一个[go.work](http://go.work)文件

$ cat go.work
go 1.18
Enter fullscreen mode Exit fullscreen mode

我们还将把我们的hello模块添加到工作区。

$ go work use ./hello
Enter fullscreen mode Exit fullscreen mode

这应该使用对我们模块[go.work](http://go.work)的引用来更新文件hello

go 1.18

use ./hello
Enter fullscreen mode Exit fullscreen mode

现在我们下载并修改stringutil包并更新Reverse函数实现

$ git clone https://go.googlesource.com/example
Cloning into 'example'...
remote: Total 204 (delta 39), reused 204 (delta 39)
Receiving objects: 100% (204/204), 467.53 KiB | 363.00 KiB/s, done.
Resolving deltas: 100% (39/39), done.
Enter fullscreen mode Exit fullscreen mode

example/stringutil/reverse.go

func Reverse(s string) string {
    return fmt.Sprintf("I can do whatever!! %s", s)
}
Enter fullscreen mode Exit fullscreen mode

最后,让我们将example包添加到我们的工作区

$ go work use ./example
$ cat go.work
go 1.18

use (
    ./example
    ./hello
)
Enter fullscreen mode Exit fullscreen mode

完美,现在如果我们运行我们的hello模块,我们会注意到该Reverse功能已被修改。

$ go run hello
I can do whatever!! Hello Workspace
Enter fullscreen mode Exit fullscreen mode

这是 Go 1.18 中一个被低估的功能,但在某些情况下非常有用。

这就是 Go 中的工作区,我们将在下一个教程中见到您。

套餐

在本教程中,我们将讨论包。

那么什么是包?

包只不过是一个包含一个或多个 Go 源文件或其他 Go 包的目录。

这意味着每个 Go 源文件必须属于一个包,并且包声明在每个源文件的顶部完成,如下所示

package <package_name>
Enter fullscreen mode Exit fullscreen mode

到目前为止,我们已经完成了 中的所有操作package main。按照惯例,可执行程序(我指的是main包中的程序)被称为 *命令,其他的则简称为包。

main包还应包含一个main()函数,该函数是一个充当可执行程序入口点的特殊函数。

让我们举个例子,创建我们自己的包custom并向其中添加一些源文件,例如code.go

package custom
Enter fullscreen mode Exit fullscreen mode

在继续之前,我们应该先讨论一下 import 和 export。和其他语言一样,Go 也有一个 import 和 export 的概念,但它非常优雅。

基本上,如果使用大写标识符定义任何值(如变量或函数),则可以将其导出并从其他包中看到。

让我们在我们的custom包中尝试一个例子

package custom

var value int = 10 // Will not be exported
var Value int = 20 // Will be exported
Enter fullscreen mode Exit fullscreen mode

我们可以看到小写标识符不会被导出,并且对于它所定义的包来说是私有的。在我们的例子中是custom包。

太棒了,但该如何导入或访问它呢?嗯,就像我们之前不知不觉就做过的那样。让我们进入main.go文件并导入我们的custom包。

这里我们可以使用之前在文件module中初始化的go.mod

---go.mod---
module example

go 1.18

---main.go--
package main

import "example/custom"

func main() {
    custom.Value
}
Enter fullscreen mode Exit fullscreen mode

注意包名称是导入路径的最后一个名称

我们也可以像这样导入多个包。

package main

import (
    "fmt"

    "example/custom"
)

func main() {
    fmt.Println(custom.Value)
}
Enter fullscreen mode Exit fullscreen mode

我们还可以为导入添加别名,以避免此类冲突

package main

import (
    "fmt"

    abcd "example/custom"
)

func main() {
    fmt.Println(abcd.Value)
}
Enter fullscreen mode Exit fullscreen mode

外部依赖项

在 Go 中,我们不仅限于使用本地包,还可以go install像之前看到的那样使用命令安装外部包。

因此,让我们下载一个简单的日志包github.com/rs/zerolog/log

$ go install github.com/rs/zerolog
Enter fullscreen mode Exit fullscreen mode
package main

import (
    "github.com/rs/zerolog/log"

    abcd "example/custom"
)

func main() {
    log.Print(abcd.Value)
}
Enter fullscreen mode Exit fullscreen mode

另外,请务必查看你安装的软件包的 Go 文档,它通常位于项目的自述文件中。Go 文档会解析源代码并生成 HTML 格式的文档。它的参考资料通常位于自述文件中。

最后,我要补充一点,Go 没有特定的“文件夹结构”约定,请始终尝试以简单直观的方式组织您的包。

所以这就是关于包的全部内容,下篇教程再见!

有用的命令

在模块讨论中,我们讨论了一些与 go 模块相关的 go 命令,现在让我们讨论一些其他重要的命令

从开始go fmt,它格式化源代码并由该语言强制执行,以便我们可以专注于我们的代码应该如何工作而不是我们的代码应该如何看起来。

$ go fmt
Enter fullscreen mode Exit fullscreen mode

这可能一开始看起来有点奇怪,特别是如果你像我一样有 javascript 或 python 背景,但坦率地说,不用担心 linting 规则还是很不错的。

接下来,我们要go vet报告包裹中可能存在的错误。

所以如果我继续犯语法错误,然后运行go vet

它应该通知我错误

$ go vet
Enter fullscreen mode Exit fullscreen mode

接下来,我们go env简单地打印所有的 go 环境信息,我们将在下一个教程中了解其中一些构建时变量。

最后,我们go doc显示了包或符号的文档,这是格式包的示例

$ go doc -src fmt Printf
Enter fullscreen mode Exit fullscreen mode

让我们使用go help命令来查看还有哪些可用的命令。

$ go help
Enter fullscreen mode Exit fullscreen mode

正如我们所见,我们有

go fix查找使用旧 API 的 Go 程序,并重写它们以使用较新的 API

go generate通常用于代码生成

go install编译并安装包和依赖项

go clean用于清理编译器生成的文件

其他一些非常重要的命令是go build和,go test但我们将在课程后面详细了解它们。

关于 go 命令的内容基本就是这样了,欢迎大家尝试!我们下篇教程再见。

建造

构建静态二进制文件是 Go 的最佳特性之一,它使我们能够高效地交付代码

go build我们可以使用命令轻松完成此操作

package main

import "fmt"

func main() {
    fmt.Println("I am a binary!")
}
Enter fullscreen mode Exit fullscreen mode
$ go build
Enter fullscreen mode Exit fullscreen mode

这应该会生成一个以我们模块名称命名的二进制文件。例如,这里有example

我们还可以指定输出

$ go build -o app
Enter fullscreen mode Exit fullscreen mode

现在要运行它,我们只需执行它

$ ./app
I am a binary!
Enter fullscreen mode Exit fullscreen mode

是的,就这么简单!

现在,让我们讨论一些重要的构建时间变量,首先是

  • GOOSGOARCH

这些环境变量有助于为不同的操作系统
和底层处理器架构构建 go 程序

我们可以使用go tool命令列出所有支持的体系结构

$ go tool dist list
android/amd64
ios/amd64
js/wasm
linux/amd64
windows/arm64
.
.
.
Enter fullscreen mode Exit fullscreen mode

这是从 macOS 构建窗口可执行文件的示例!

$ GOOS=windows GOARCH=amd64 go build -o app.exe
Enter fullscreen mode Exit fullscreen mode
  • CGO_ENABLED

这个变量允许我们配置CGO,这是 Go 中调用 C 代码的一种方式。

这有助于我们生成无需任何外部依赖即可运行的静态链接二进制文件。

当我们想在具有最少外部依赖性的 docker 容器中运行我们的 go 二进制文件时,这非常有用。

以下是如何使用它的示例

$ CGO_ENABLED=0 go build -o app
Enter fullscreen mode Exit fullscreen mode

指针

在本教程中,我们将讨论指针。那么什么是指针?

简单的定义,指针是一个用于存储另一个变量的内存地址的变量。

指针

可以这样使用

var x *T
Enter fullscreen mode Exit fullscreen mode

其中 T 是类型,例如intstringfloat等等

让我们尝试一个简单的例子并看看它的实际效果

package main

import "fmt"

func main() {
    var p *int

    fmt.Println(p)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
nil
Enter fullscreen mode Exit fullscreen mode

嗯,这会打印nil,但是什么nil

因此,nil 是 Go 中的预声明标识符,表示指针、接口、通道、映射和切片的零值。

这就像我们在变量和数据类型部分学到的一样,我们看到未初始化的int零值为 0,abool为 false,等等。

好的,现在让我们给指针赋值

package main

import "fmt"

func main() {
    a := 10

    var p *int = &a

    fmt.Println("address:", p)
}
Enter fullscreen mode Exit fullscreen mode

我们使用&与号运算符来引用变量的内存地址。

$ go run main.go
0xc0000b8000
Enter fullscreen mode Exit fullscreen mode

这必须是变量的内存地址的值a

取消引用

我们还可以使用*星号运算符来访问指针指向的变量中存储的值。这称为解引用

例如,我们可以使用星号运算符a通过指针访问变量的值p*

package main

import "fmt"

func main() {
    a := 10

    var p *int = &a

    fmt.Println("address:", p)
    fmt.Println("value:", *p)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
address: 0xc000018030
value: 10
Enter fullscreen mode Exit fullscreen mode

我们不仅可以访问它,还可以通过指针改变它

package main

import "fmt"

func main() {
    a := 10

    var p *int = &a

    fmt.Println("before", a)
    fmt.Println("address:", p)

    *p = 20
    fmt.Println("after:", a)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
before 10
address: 0xc000192000
after: 20
Enter fullscreen mode Exit fullscreen mode

我认为这很棒!

指针作为函数参数

当我们需要通过引用传递一些数据时,指针也可以用作函数的参数。

这是一个例子

myFunction(&a)
...

func myFunction(ptr *int) {}
Enter fullscreen mode Exit fullscreen mode

新功能

还有另一种初始化指针的方法。我们可以使用一个new函数,该函数接受一个类型作为参数,分配足够的内存来容纳该类型的值,并返回一个指向该类型的指针。

这是一个例子

package main

import "fmt"

func main() {
    p := new(int)
    *p = 100

    fmt.Println("value", *p)
    fmt.Println("address", p)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
value 100
address 0xc000018030
Enter fullscreen mode Exit fullscreen mode

指向指针的指针

这里有一个有趣的想法……我们能创建一个指向指针的指针吗?答案是肯定的!

是的,我们可以。

package main

import "fmt"

func main() {
    p := new(int)
    *p = 100

    p1 := &p

    fmt.Println("P value", *p, " address", p)
    fmt.Println("P1 value", *p1, " address", p)

    fmt.Println("Dereferenced value", **p1)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
P value 100  address 0xc0000be000
P1 value 0xc0000be000  address 0xc0000be000
Dereferenced value 100
Enter fullscreen mode Exit fullscreen mode

注意的值如何p1与的地址匹配p

另外,重要的是要知道 Go 中的指针不像 C 或 C++ 那样支持指针运算。

    p1 := p * 2 // Compiler Error: invalid operation
Enter fullscreen mode Exit fullscreen mode

==但是,我们可以使用运算符比较两个相同类型的指针是否相等

p := &a
p1 := &a

fmt.Println(p == p1)
Enter fullscreen mode Exit fullscreen mode

但为什么?

这就给我们带来了一个价值百万美元的问题:我们为什么需要指针?

嗯,这个问题没有明确的答案,指针只是另一个有用的功能,它可以帮助我们有效地改变数据,而无需复制大量数据。

并且可以应用于大量用例。

最后,我要补充一点,如果您来自一种没有指针概念的语言,请不要惊慌,并尝试形成一个指针如何工作的心理模型。

完美!我们了解了指针及其用例,现在让我们继续下一个主题。

结构体

在本教程中,我们将学习结构。

因此,Astruct是一个用户定义类型,包含命名字段的集合。它的作用是将相关数据组合在一起,形成一个单元。

如果您来自面向对象的背景,请将结构视为支持组合但不支持继承的轻量级类。

定义

我们可以struct像这样定义

type Person struct {}
Enter fullscreen mode Exit fullscreen mode

我们使用type关键字来引入新类型,然后是名称,然后是struct关键字来表明我们正在定义一个结构体

现在让我们给它一些字段

type Person struct {
    FirstName string
    LastName  string
    Age       int
}
Enter fullscreen mode Exit fullscreen mode

如果字段类型相同,我们也可以折叠它们

type Person struct {
    FirstName, LastName string
    Age                 int
}
Enter fullscreen mode Exit fullscreen mode

声明和初始化

现在我们有了结构体,我们可以像其他数据类型一样声明它

func main() {
    var p1 Person

    fmt.Println("Person 1:", p1)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Person 1: {  0}
Enter fullscreen mode Exit fullscreen mode

我们可以看到,所有结构体字段都初始化为零值。因此,FirstNameLastName被设置为“”空字符串,Age被设置为 0。

我们也可以简单地将其初始化为“结构文字”

func main() {
    var p1 Person

    fmt.Println("Person 1:", p1)

    var p2 = Person{FirstName: "Karan", LastName: "Pratap Singh", Age: 22}

    fmt.Println("Person 2:", p2)
}
Enter fullscreen mode Exit fullscreen mode

为了便于阅读,我们可以用新行分隔,但这也需要一个逗号

    var p2 = Person{
        FirstName: "Karan",
        LastName:  "Pratap Singh",
        Age:       22,
    }
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Person 1: {  0}
Person 2: {Karan Pratap Singh 22}
Enter fullscreen mode Exit fullscreen mode

我们也可以只初始化字段的子集

func main() {
    var p1 Person

    fmt.Println("Person 1:", p1)

    var p2 = Person{
        FirstName: "Karan",
        LastName:  "Pratap Singh",
        Age:       22,
    }

    fmt.Println("Person 2:", p2)

    var p3 = Person{
        FirstName: "Tony",
        LastName:  "Stark",
    }

    fmt.Println("Person 3:", p3)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Person 1: {  0}
Person 2: {Karan Pratap Singh 22}
Person 3: {Tony Stark 0}
Enter fullscreen mode Exit fullscreen mode

我们可以看到,3号人的年龄字段已经默认为零值。

不带字段名

Go 结构体也支持无字段名的初始化

func main() {
    var p1 Person

    fmt.Println("Person 1:", p1)

    var p2 = Person{
        FirstName: "Karan",
        LastName:  "Pratap Singh",
        Age:       22,
    }

    fmt.Println("Person 2:", p2)

    var p3 = Person{
        FirstName: "Tony",
        LastName:  "Stark",
    }

    fmt.Println("Person 3:", p3)

    var p4 = Person{"Bruce", "Wayne"}

    fmt.Println("Person 4:", p4)
}
Enter fullscreen mode Exit fullscreen mode

但这里有一个问题,我们需要在初始化期间提供所有值,否则它将失败

$ go run main.go
# command-line-arguments
./main.go:30:27: too few values in Person{...}
Enter fullscreen mode Exit fullscreen mode
    var p4 = Person{"Bruce", "Wayne", 40}

    fmt.Println("Person 4:", p4)
Enter fullscreen mode Exit fullscreen mode

我们还可以声明一个匿名结构体

func main() {
    var p1 Person

    fmt.Println("Person 1:", p1)

    var p2 = Person{
        FirstName: "Karan",
        LastName:  "Pratap Singh",
        Age:       22,
    }

    fmt.Println("Person 2:", p2)

    var p3 = Person{
        FirstName: "Tony",
        LastName:  "Stark",
    }

    fmt.Println("Person 3:", p3)

    var p4 = Person{"Bruce", "Wayne", 40}

    fmt.Println("Person 4:", p4)

    var a = struct {
        Name string
    }{"Golang"}

    fmt.Println("Anonymous:", a)
}
Enter fullscreen mode Exit fullscreen mode

访问字段

让我们稍微整理一下示例,看看如何访问各个字段

func main() {
    var p = Person{
        FirstName: "Karan",
        LastName:  "Pratap Singh",
        Age:       22,
    }

    fmt.Println("FirstName", p.FirstName)
}
Enter fullscreen mode Exit fullscreen mode

我们也可以创建指向结构的指针

func main() {
    var p = Person{
        FirstName: "Karan",
        LastName:  "Pratap Singh",
        Age:       22,
    }

    ptr := &p

    fmt.Println((*ptr).FirstName)
    fmt.Println(ptr.FirstName)
}
Enter fullscreen mode Exit fullscreen mode

这两个语句是相等的,因为在 Go 中我们不需要显式地取消引用指针

我们还可以使用内置new函数

func main() {
    p := new(Person)

    p.FirstName = "Karan"
    p.LastName = "Pratap Singh"
    p.Age = 22

    fmt.Println("Person", p)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Person &{Karan Pratap Singh 22}
Enter fullscreen mode Exit fullscreen mode

附注:如果两个结构体的所有对应字段也相等,则它们相等

func main() {
    var p1 = Person{"a", "b", 20}
    var p2 = Person{"a", "b", 20}

    fmt.Println(p1 == p2)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
true
Enter fullscreen mode Exit fullscreen mode

导出的字段

现在让我们了解一下结构体中的导出字段和非导出字段。与变量和函数的规则相同,如果结构体字段使用小写标识符声明,则它将不会被导出,并且仅在其定义的包中可见。

type Person struct {
    FirstName, LastName  string
    Age                  int
    zipCode              string
}
Enter fullscreen mode Exit fullscreen mode

因此,该zipCode字段不会被导出。同样,Person如果我们重命名结构体,person它也会同样不会被导出。

type person struct {
    FirstName, LastName  string
    Age                  int
    zipCode              string
}
Enter fullscreen mode Exit fullscreen mode

嵌入和组合

正如我们之前讨论过的,Go 不一定支持继承,但我们可以通过嵌入做类似的事情

type Person struct {
    FirstName, LastName  string
    Age                  int
}

type SuperHero struct {
    Person
    Power   int
}
Enter fullscreen mode Exit fullscreen mode

因此,我们的新结构体将具有原始结构体的所有属性。并且其行为应该与我们的正常结构体相同。

func main() {
    s := SuperHero{}

    s.FirstName = "Bruce"
    s.LastName = "Wayne"
    s.Age = 40
    s.Alias = "batman"

    fmt.Println(s)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
{{Bruce Wayne 40} batman}
Enter fullscreen mode Exit fullscreen mode

然而,通常不建议这样做,大多数情况下,组合更受欢迎。因此,我们不会将其嵌入,而是将其定义为普通字段。

type Person struct {
    FirstName, LastName  string
    Age                  int
}

type SuperHero struct {
    Person  Person
    Alias   string
}
Enter fullscreen mode Exit fullscreen mode

因此,我们也可以用组合来重写我们的例子

func main() {
    p := Person{"Bruce", "Wayne", 40}
    s := SuperHero{p, "batman"}

    fmt.Println(s)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
{{Bruce Wayne 40} batman}
Enter fullscreen mode Exit fullscreen mode

再次强调,这里没有对错之分,但尽管如此,嵌入有时还是很有用的。

结构标签

结构标签只是一个标签,它允许我们将元数据信息附加到字段,该字段可用于使用reflect包的自定义行为。

让我们学习如何定义结构标签。

type Animal struct {
    Name    string `key:"value1"`
    Age     int    `key:"value2"`
}
Enter fullscreen mode Exit fullscreen mode

您经常会在编码包中找到标签,例如 XML、JSON、YAML、ORM 和配置管理。

这是 JSON 编码器的标签示例。

type Animal struct {
    Name    string `json:"name"`
    Age     int    `json:"age"`
}
Enter fullscreen mode Exit fullscreen mode

特性

最后,我们来讨论一下结构体的属性。

结构体是值类型。当我们将一个struct变量赋值给另一个变量时,struct会创建并赋值该变量的一个新副本。

类似地,当我们将 a 传递struct给另一个函数时,该函数会获得它自己的 的副本struct

package main

import "fmt"

type Point struct {
    X, Y float64
}

func main() {
    p1 := Point{1, 2}
    p2 := p1 // Copy of p1 is assigned to p2

    p2.X = 2

    fmt.Println(p1) // Output: {1 2}
    fmt.Println(p2) // Output: {2 2}
}
Enter fullscreen mode Exit fullscreen mode

空结构占用零字节存储空间

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var s struct{}
    fmt.Println(unsafe.Sizeof(s)) // Output: 0
}
Enter fullscreen mode Exit fullscreen mode

好了,关于的讨论到此结束structs。接下来,我们将学习如何使用方法扩展结构体。

方法

让我们来讨论方法,或者有时也称为函数接收器。

从技术上讲,Go 不是面向对象的编程语言。它没有类、对象和继承。

然而,Go 有类型。而且,你可以在类型上定义方法

方法只不过是一个带有特殊接收者参数的函数。让我们看看如何声明方法

func (variable T) Name(params) (returnTypes) {}
Enter fullscreen mode Exit fullscreen mode

接收者参数具有名称和类型。它出现在func关键字和方法名称之间

例如,我们定义一个Car结构体

type Car struct {
    Name string
    Year int
}
Enter fullscreen mode Exit fullscreen mode

现在让我们定义一个方法,IsLatest它可以告诉我们一辆汽车是否在过去 5 年内制造

func (c Car) IsLatest() bool {
    return c.Year >= 2017
}
Enter fullscreen mode Exit fullscreen mode

Car如你所见,我们可以使用接收变量来访问实例c。我喜欢把它看作是this面向对象世界中的关键字。

现在我们应该能够在初始化结构后调用此方法,就像我们在其他语言中对类所做的那样

func main() {
    c := Car{"Tesla", 2021}

    fmt.Println("IsLatest", c.IsLatest())
}
Enter fullscreen mode Exit fullscreen mode

带有指针接收器的方法

我们之前看到的所有例子都有一个值接收器。

使用值接收者时,方法操作的是传递给它的值的副本。因此,方法内部对接收者所做的任何修改对调用者都是不可见的。

例如,让我们创建另一个方法,UpdateName它将更新Car

func (c Car) UpdateName(name string) {
    c.Name = name
}
Enter fullscreen mode Exit fullscreen mode

现在让我们运行这个

func main() {
    c := Car{"Tesla", 2021}

    c.UpdateName("Toyota")
    fmt.Println("Car:", c)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Car: {Tesla 2021}
Enter fullscreen mode Exit fullscreen mode

好像名称没有更新,所以现在让我们将接收器切换为指针类型并重试

func (c *Car) UpdateName(name string) {
    c.Name = name
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Car: {Toyota 2021}
Enter fullscreen mode Exit fullscreen mode

正如预期的那样,带有指针接收者的方法可以修改接收者指向的值。此类修改对于方法的调用者也是可见的。

特性

我们还来看看这些方法的一些属性!

  • Go 足够智能,能够正确解释我们的函数调用,因此,指针接收器方法调用只是 Go 为了方便而提供的语法糖。
(&c).UpdateName(...)
Enter fullscreen mode Exit fullscreen mode
  • 如果我们不使用接收器的变量部分,我们也可以省略它
func (Car) UpdateName(...) {}
Enter fullscreen mode Exit fullscreen mode
  • 方法不仅限于结构体,还可以用于非结构体类型
package main

import "fmt"

type MyInt int

func (i MyInt) isGreater(value int) bool {
    return i > MyInt(value)
}

func main() {
    i := MyInt(10)

    fmt.Println(i.isGreater(5))
}
Enter fullscreen mode Exit fullscreen mode

为什么使用方法而不是函数?

那么问题是,为什么是方法而不是函数?

一如既往,这个问题没有特定的答案,也不存在哪一种更好。相反,它们应该根据具体情况适当使用。

我现在能想到的一件事是方法可以帮助我们避免命名冲突。

由于方法与特定类型相关,因此我们可以为多个接收器使用相同的方法名称。

但一般来说,这可能只是取决于偏好?例如“方法调用比函数调用更容易阅读和理解”或反之亦然。

至此,我们关于方法的讨论就结束了,下篇教程再见

数组和切片

在本教程中,我们将学习 Go 中的数组和切片。

数组

那么数组是什么?

数组是相同类型元素的固定大小集合。数组元素按顺序存储,可以使用其index

大批

宣言

我们可以按如下方式声明一个数组

var a [n]T
Enter fullscreen mode Exit fullscreen mode

n是长度,T可以是任何类型,如整数、字符串或用户定义的结构。

现在,让我们声明一个长度为 4 的整数数组并打印它。

func main() {
    var arr [4]int

    fmt.Println(arr)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
[0 0 0 0]
Enter fullscreen mode Exit fullscreen mode

默认情况下,所有数组元素都用相应数组类型的零值初始化。

初始化

我们还可以使用数组文字初始化数组

var a [n]T = [n]T{V1, V2, ... Vn}
Enter fullscreen mode Exit fullscreen mode
func main() {
    var arr = [4]int{1, 2, 3, 4}

    fmt.Println(arr)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
[1 2 3 4]
Enter fullscreen mode Exit fullscreen mode

我们甚至可以做一个简写声明

...
arr := [4]int{1, 2, 3, 4}
Enter fullscreen mode Exit fullscreen mode

使用权

与其他语言类似,我们可以使用访问元素,index因为它们是按顺序存储的

func main() {
    arr := [4]int{1, 2, 3, 4}

    fmt.Println(arr[0])
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
1
Enter fullscreen mode Exit fullscreen mode

迭代

现在,我们来讨论迭代。

因此有多种方法可以迭代数组。

第一个是使用 for 循环和len函数来获取数组的长度

func main() {
    arr := [4]int{1, 2, 3, 4}

    for i := 0; i < len(arr); i++ {
        fmt.Printf("Index: %d, Element: %d\n", i, arr[i])
    }
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Index: 0, Element: 1
Index: 1, Element: 2
Index: 2, Element: 3
Index: 3, Element: 4
Enter fullscreen mode Exit fullscreen mode

另一种方法是使用rangefor 循环中的关键字

func main() {
    arr := [4]int{1, 2, 3, 4}

    for i, e := range arr {
        fmt.Printf("Index: %d, Element: %d\n", i, e)
    }
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Index: 0, Element: 1
Index: 1, Element: 2
Index: 2, Element: 3
Index: 3, Element: 4
Enter fullscreen mode Exit fullscreen mode

我们可以看到,我们的示例与以前一样工作。

但 range 关键字用途非常广泛,可以以多种方式使用。

for i, e := range arr {} // Normal usage of range

for _, e := range arr {} // Omit index with _ and use element

for i := range arr {} // Use index only

for range arr {} // Simply loop over the array
Enter fullscreen mode Exit fullscreen mode

多维

到目前为止,我们创建的所有数组都是一维的。我们也可以在 Go 中创建多维数组。

让我们举个例子

func main() {
    arr := [2][4]int{
        {1, 2, 3, 4},
        {5, 6, 7, 8},
    }

    for i, e := range arr {
        fmt.Printf("Index: %d, Element: %d\n", i, e)
    }
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Index: 0, Element: [1 2 3 4]
Index: 1, Element: [5 6 7 8]
Enter fullscreen mode Exit fullscreen mode

...我们还可以让编译器通过使用省略号而不是长度来推断数组的长度

func main() {
    arr := [...][4]int{
        {1, 2, 3, 4},
        {5, 6, 7, 8},
    }

    for i, e := range arr {
        fmt.Printf("Index: %d, Element: %d\n", i, e)
    }
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Index: 0, Element: [1 2 3 4]
Index: 1, Element: [5 6 7 8]
Enter fullscreen mode Exit fullscreen mode

特性

现在我们来讨论一下数组的一些属性。

数组的长度是其类型的一部分。因此,数组ab是完全不同的类型,我们不能将一个赋值给另一个。

这也意味着我们不能调整数组的大小,因为调整数组的大小意味着改变其类型。

package main

func main() {
    var a = [4]int{1, 2, 3, 4}
    var b [2]int = a // Error, cannot use a (type [4]int) as type [2]int in assignment
}
Enter fullscreen mode Exit fullscreen mode

Go 中的数组是值类型,这与 C、C++ 和 Java 等其他语言中的数组是引用类型不同。

这意味着当我们将一个数组分配给一个新变量或将一个数组传递给一个函数时,整个数组都会被复制。

因此,如果我们对这个复制的数组进行任何更改,原始数组将不会受到影响并将保持不变。

package main

import "fmt"

func main() {
    var a = [7]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
    var b = a // Copy of a is assigned to b

    b[0] = "Monday"

    fmt.Println(a) // Output: [Mon Tue Wed Thu Fri Sat Sun]
    fmt.Println(b) // Output: [Monday Tue Wed Thu Fri Sat Sun]
}
Enter fullscreen mode Exit fullscreen mode

切片

我知道你在想什么,数组很有用,但由于其固定大小的限制而有点不灵活。

这给我们带来了切片,那么什么是切片?

切片是数组的一部分。切片基于数组构建,提供更强大的功能、灵活性和便捷性。

片

切片由三部分组成:

  • 指向底层数组的指针引用。
  • 切片包含的数组段的长度。
  • 容量是段可以增长的最大大小。

就像函数一样len,我们可以使用内置cap函数来确定切片的容量。以下是一个例子:

package main

import "fmt"

func main() {
    a := [5]int{20, 15, 5, 30, 25}

    s := a[1:4]

    // Output: Array: [20 15 5 30 25], Length: 5, Capacity: 5
    fmt.Printf("Array: %v, Length: %d, Capacity: %d\n", a, len(a), cap(a))

    // Output: Slice [15 5], Length: 3, Capacity: 4
    fmt.Printf("Slice: %v, Length: %d, Capacity: %d", s, len(s), cap(s))
}
Enter fullscreen mode Exit fullscreen mode

别担心,我们将详细讨论这里显示的所有内容。

宣言

让我们看看如何声明切片

var s []T
Enter fullscreen mode Exit fullscreen mode

正如我们所见,我们不需要指定任何长度

让我们声明一个整数切片并看看它是如何工作的

func main() {
    var s []string

    fmt.Println(s)
    fmt.Println(s == nil)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
[]
true
Enter fullscreen mode Exit fullscreen mode

因此,与数组不同,切片的零值是nil

初始化

初始化切片的方法有很多种。一种方法是使用内置make函数

make([]T, len, cap) []T
Enter fullscreen mode Exit fullscreen mode
func main() {
var s = make([]string, 0, 0)

    fmt.Println(s)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
[]
Enter fullscreen mode Exit fullscreen mode

与数组类似,我们可以使用切片文字来初始化切片

func main() {
    var s = []string{"Go", "TypeScript"}

    fmt.Println(s)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
[Go TypeScript]
Enter fullscreen mode Exit fullscreen mode

另一种方法是从数组创建切片。由于切片是数组的一部分,我们可以从索引到索引处创建一个切片,如下所示lowhigh

a[low:high]
Enter fullscreen mode Exit fullscreen mode
func main() {
    var a = [4]string{
        "C++",
        "Go",
        "Java",
        "TypeScript",
    }

    s1 := a[0:2] // Select from 0 to 2
    s2 := a[:3]  // Select first 3
    s3 := a[2:]  // Select last 2

    fmt.Println("Array:", a)
    fmt.Println("Slice 1:", s1)
    fmt.Println("Slice 2:", s2)
    fmt.Println("Slice 3:", s3)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Array: [C++ Go Java TypeScript]
Slice 1: [C++ Go]
Slice 2: [C++ Go Java]
Slice 3: [Java TypeScript]
Enter fullscreen mode Exit fullscreen mode

缺少低指数意味着 0,缺少高指数意味着len(a)

这里要注意的是,我们也可以从其他切片创建切片,而不仅仅是数组

    var a = []string{
        "C++",
        "Go",
        "Java",
        "TypeScript",
    }
Enter fullscreen mode Exit fullscreen mode

迭代

我们可以像迭代数组一样迭代切片,通过使用带有len函数或range关键字的 for 循环。

函数

那么现在我们来谈谈 Go 中提供的内置切片函数。

  • 复制

copy()函数将元素从一个切片复制到另一个切片。它需要 2 个切片、一个目标切片和一个源切片作为参数。它还返回复制的元素数量。

func copy(dst, src []T) int
Enter fullscreen mode Exit fullscreen mode

让我们看看如何使用它

func main() {
    s1 := []string{"a", "b", "c", "d"}
    s2 := make([]string, 0)

    e := copy(s2, s1)

    fmt.Println("Src:", s1)
    fmt.Println("Dst:", s2)
    fmt.Println("Elements:", e)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Src: [a b c d]
Dst: [a b c d]
Elements: 4
Enter fullscreen mode Exit fullscreen mode

正如预期的那样,源切片中的 4 个元素被复制到目标切片

  • 附加

现在让我们看看如何使用内置append函数将数据附加到切片中,该函数将新元素附加到给定切片的末尾。

它接受一个切片和一个可变数量的参数,然后返回一个包含所有元素的新切片。

append(slice []T, elems ...T) []T
Enter fullscreen mode Exit fullscreen mode

让我们在一个示例中尝试一下,将元素附加到切片中

func main() {
    s1 := []string{"a", "b", "c", "d"}

    s2 := append(a1, "e", "f")

    fmt.Println("a1:", a1)
    fmt.Println("a2:", a2)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
a1: [a b c d]
a2: [a b c d e f]
Enter fullscreen mode Exit fullscreen mode

我们可以看到,新元素被附加,并且返回了一个新的切片。

但是如果给定的切片没有足够的容量容纳新元素,则会分配一个具有更大容量的新底层数组。

现有切片的底层数组中的所有元素都被复制到这个新数组中,然后附加新元素。

特性

最后,我们来讨论一下切片的一些属性。

与数组不同,切片是引用类型。

这意味着修改切片的元素将修改引用数组中的相应元素。

package main

import "fmt"

func main() {
    a := [7]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}

    s := a[0:2]

    s[0] = "Sun"

    fmt.Println(a) // Output: [Sun Tue Wed Thu Fri Sat Sun]
    fmt.Println(s) // Output: [Sun Tue]
}
Enter fullscreen mode Exit fullscreen mode

切片也可以与可变类型一起使用。

package main

import "fmt"

func main() {
    values := []int{1, 2, 3}
    sum := add(values...)
    fmt.Println(sum)
}

func add(values ...int) int {
    sum := 0
    for _, v := range values {
        sum += v
    }

    return sum
}
Enter fullscreen mode Exit fullscreen mode

好了,这就是 Go 中的数组和切片的全部内容,下期再见!

地图

因此,Go 提供了一个内置的地图类型,我们将学习如何使用它。

但问题是,地图是什么?我们为什么需要它们?

地图

嗯,Map 是键值对的无序集合。它将键映射到值。Map 中的键是唯一的,但值可能不唯一。

它用于根据键快速查找、检索和删除数据。它是最常用的数据结构之一。

宣言

让我们从声明开始

使用以下语法声明地图

var m map[K]V
Enter fullscreen mode Exit fullscreen mode

其中K,键类型是,V值类型是

例如,我们可以这样声明一个string键到int值的映射

func main() {
    var m map[string]int

    fmt.Println(m)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
nil
Enter fullscreen mode Exit fullscreen mode

我们可以看出,映射的零值是nil

映射nil没有键。此外,任何向映射添加键的尝试nil都会导致运行时错误。

初始化

初始化地图的方法有多种。

使函数

我们可以使用内置make函数,它为引用的数据类型分配内存并初始化其底层数据结构。

func main() {
    var m = make(map[string]int)

    fmt.Println(m)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
map[]
Enter fullscreen mode Exit fullscreen mode

地图文字

另一种方法是使用地图文字。

func main() {
    var m = map[string]int{
        "a": 0,
    "b": 1,
    }

    fmt.Println(m)
}
Enter fullscreen mode Exit fullscreen mode

请注意,最后一个逗号是必需的

$ go run main.go
map[a:0 b:1]
Enter fullscreen mode Exit fullscreen mode

与往常一样,我们也可以使用自定义类型

type User struct {
    Name string
}

func main() {
    var m = map[string]User{
        "a": User{"Peter"},
        "b": User{"Seth"},
    }

    fmt.Println(m)
}
Enter fullscreen mode Exit fullscreen mode

我们甚至可以删除值类型,Go 会解决它!

var m = map[string]User{
    "a": {"Peter"},
    "b": {"Seth"},
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
map[a:{Peter} b:{Seth}]
Enter fullscreen mode Exit fullscreen mode

添加

现在,让我们看看如何向地图添加值。

func main() {
    var m = map[string]User{
        "a": {"Peter"},
        "b": {"Seth"},
    }

    m["c"] = User{"Steve"}

    fmt.Println(m)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
map[a:{Peter} b:{Seth} c:{Steve}]
Enter fullscreen mode Exit fullscreen mode

取回

我们还可以使用键从地图中检索值

...
c := m["c"]
fmt.Println("Key c:", c)
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
key c: {Steve}
Enter fullscreen mode Exit fullscreen mode

如果我们使用地图中不存在的键会怎么样?

...
d := m["d"]
fmt.Println("Key d:", d)
Enter fullscreen mode Exit fullscreen mode

是的,你猜对了!我们将获得地图值类型的零值。

$ go run main.go
Key c: {Steve}
Key d: {}
Enter fullscreen mode Exit fullscreen mode

存在

当你检索分配给给定键的值时,它还会返回一个额外的布尔值。true如果键存在,则返回布尔值,false否则返回布尔值。

让我们在示例中尝试一下

...
c, ok := m["c"]
fmt.Println("Key c:", c, ok)

d, ok := m["d"]
fmt.Println("Key d:", d, ok)
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Key c: {Steve} Present: true
Key d: {} Present: false
Enter fullscreen mode Exit fullscreen mode

更新

我们还可以通过简单地重新分配键来更新键的值

...
m["a"] = "Roger"
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
map[a:{Roger} b:{Seth} c:{Steve}]
Enter fullscreen mode Exit fullscreen mode

删除

或者,我们可以使用内置delete函数删除该键。

语法如下

...
delete(m,
Enter fullscreen mode Exit fullscreen mode

第一个参数是地图,第二个参数是我们要删除的键。

delete()函数不返回任何值。此外,如果键在映射中不存在,它也不会执行任何操作。

$ go run main.go
map[a:{Roger} c:{Steve}]
Enter fullscreen mode Exit fullscreen mode

迭代

与数组或切片类似,我们可以使用range关键字

package main

import "fmt"

func main() {
    var m = map[string]User{
        "a": {"Peter"},
        "b": {"Seth"},
    }

    m["c"] = User{"Steve"}

    for key, value := range m {
        fmt.Println("Key: %s, Value: %v", key, value)
    }
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Key: c, Value: {Steve}
Key: a, Value: {Peter}
Key: b, Value: {Seth}
Enter fullscreen mode Exit fullscreen mode

请注意,映射是一个无序集合,因此每次迭代映射时,其迭代顺序都不能保证相同。

特性

最后,我们来谈谈地图属性。

映射是引用类型,这意味着当我们将映射分配给新变量时,它们都引用相同的底层数据结构。

因此,一个变量所做的更改对另一个变量也是可见的。

package main

import "fmt"

type User struct {
    Name string
}

func main() {
    var m1 = map[string]User{
        "a": {"Peter"},
        "b": {"Seth"},
    }

    m2 := m1
    m2["c"] = User{"Steve"}

    fmt.Println(m1) // Output: map[a:{Peter} b:{Seth} c:{Steve}]
    fmt.Println(m2) // Output: map[a:{Peter} b:{Seth} c:{Steve}]
}
Enter fullscreen mode Exit fullscreen mode

好了,关于地图的讨论就到此结束,下篇教程再见!

接口

在本节中,我们来讨论一下接口。

什么是接口?

Go 中的接口是一种使用一组方法签名定义的抽象类型。接口定义了类似类型对象的行为。

这里,行为是一个关键词,我们将很快讨论。

让我们举一个例子来更好地理解这一点。

现实世界中接口最好的例子之一就是电源插座。想象一下,我们需要将不同的设备连接到电源插座。

无接口

让我们尝试实现它。以下是我们将使用的设备类型。

type mobile struct {
    brand string
}

type laptop struct {
    cpu string
}

type toaster struct {
    amount int
}

type kettle struct {
    quantity string
}

type socket struct{}
Enter fullscreen mode Exit fullscreen mode

现在让我们Draw在一个类型上定义一个方法,比如说mobile。这里我们将简单地打印该类型的属性。

func (m mobile) Draw(power int) {
    fmt.Printf("%T -> brand: %s, power: %d", m, m.brand, power)
}
Enter fullscreen mode Exit fullscreen mode

太好了,现在我们将在接受我们的类型作为参数的类型Plug上定义方法。socketmobile

func (socket) Plug(device mobile, power int) {
    device.Draw(power)
}
Enter fullscreen mode Exit fullscreen mode

让我们尝试在函数中将类型“连接”“插入”mobile到我们的socket类型中main

package main

import "fmt"

func main() {
    m := mobile{"Apple"}

    s := socket{}
    s.Plug(m, 10)
}
Enter fullscreen mode Exit fullscreen mode

如果我们运行它,我们会看到以下内容

$ go run main.go
main.mobile -> brand: Apple, power: 10
Enter fullscreen mode Exit fullscreen mode

这很有趣,但现在我们想连接**我们的laptop类型。

package main

import "fmt"

func main() {
    m := mobile{"Apple"}
    l := laptop{"Intel i9"}

    s := socket{}

    s.Plug(m, 10)
    s.Plug(l, 50) // Error: cannot use l as mobile value in argument
}
Enter fullscreen mode Exit fullscreen mode

正如我们所见,这将引发错误。

我们现在该做什么?定义另一个方法?比如PlugLaptop

当然,但是每次我们添加新的设备类型时,我们也需要向套接字类型添加新的方法,这并不理想。

这就是需要解决的问题interface。本质上,我们想要定义一份将来必须实施的合同。

我们可以简单地定义一个接口,例如,PowerDrawer并在我们的函数中使用它Plug来允许任何满足标准的设备,即该类型必须具有与Draw接口所需的签名匹配的方法。

无论如何,套接字不需要了解我们的设备的任何信息,只需调用该Draw方法即可。

界面

现在让我们尝试实现我们的PowerDrawer接口。它看起来是这样的。

惯例是在名称中使用“-er”作为后缀。正如我们之前讨论过的,接口应该只描述预期的行为。在我们的例子中,也就是Draw方法。

接口实现

type PowerDrawer interface {
    Draw(power int)
}
Enter fullscreen mode Exit fullscreen mode

现在我们需要更新我们的Plug方法来接受实现接口的设备PowerDrawer作为参数。

func (socket) Plug(device PowerDrawer, power int) {
    device.Draw(power)
}
Enter fullscreen mode Exit fullscreen mode

为了满足接口,我们可以简单地Draw向所有设备类型添加方法。

type mobile struct {
    brand string
}

func (m mobile) Draw(power int) {
    fmt.Printf("%T -> brand: %s, power: %d\n", m, m.brand, power)
}

type laptop struct {
    cpu string
}

func (l laptop) Draw(power int) {
    fmt.Printf("%T -> cpu: %s, power: %d\n", l, l.cpu, power)
}

type toaster struct {
    amount int
}

func (t toaster) Draw(power int) {
    fmt.Printf("%T -> amount: %d, power: %d\n", t, t.amount, power)
}

type kettle struct {
    quantity string
}

func (k kettle) Draw(power int) {
    fmt.Printf("%T -> quantity: %s, power: %d\n", k, k.quantity, power)
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以借助我们的接口将所有设备连接到插座!

func main() {
    m := mobile{"Apple"}
    l := laptop{"Intel i9"}
    t := toaster{4}
    k := kettle{"50%"}

    s := socket{}

    s.Plug(m, 10)
    s.Plug(l, 50)
    s.Plug(t, 30)
    s.Plug(k, 25)
}
Enter fullscreen mode Exit fullscreen mode

正如我们预期的那样,它确实有效。

$ go run main.go
main.mobile -> brand: Apple, power: 10
main.laptop -> cpu: Intel i9, power: 50
main.toaster -> amount: 4, power: 30
main.kettle -> quantity: Half Empty, power: 25
Enter fullscreen mode Exit fullscreen mode

但为什么这被认为是如此强大的概念?

嗯,接口可以帮助我们解耦类型。例如,因为我们有接口,所以我们不需要更新socket实现。我们只需定义一个带有Draw方法的新设备类型即可。

与其他语言不同,Go 的接口是隐式实现的,因此我们不需要像implements关键字这样的实现。这意味着,当一个类型拥有接口的“所有方法”时,它就自动满足该接口。

空接口

接下来我们来谈谈空接口。空接口可以接受任何类型的值。

以下是我们的声明方式。

var x interface{}
Enter fullscreen mode Exit fullscreen mode

但是我们为什么需要它?

空接口可用于处理未知类型的值。

以下是一些示例:

  • 从 API 读取异构数据
  • 未知类型的变量,例如fmt.Prinln函数中的变量

要使用类型为 empty 的值interface{},我们可以使用类型断言类型开关来确定该值的类型。

类型断言

类型断言提供对接口值的底层具体值的访问。

例如

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)
}
Enter fullscreen mode Exit fullscreen mode

该语句断言接口值具有具体类型,并将底层类型值分配给变量。

我们还可以测试接口值是否具有特定类型。

类型断言可以返回两个值:

  • 第一个是基础价值
  • 第二个是布尔值,报告断言是否成功。
s, ok := i.(string)
fmt.Println(s, ok)
Enter fullscreen mode Exit fullscreen mode

这可以帮助我们测试接口值是否具有特定类型。

从某种程度上来说,这类似于我们从地图中读取值的方式。

如果不是这种情况,ok则将为 false,并且值将是该类型的零值,并且不会发生恐慌。

f, ok := i.(float64)
fmt.Println(f, ok)
Enter fullscreen mode Exit fullscreen mode

但是如果接口不包含该类型,该语句将引发恐慌。

f = i.(float64)
fmt.Println(f) // Panic!
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
hello
hello true
0 false
panic: interface conversion: interface {} is string, not float64
Enter fullscreen mode Exit fullscreen mode

类型切换

这里,switch可以使用语句来确定空类型变量的类型interface{}

var t interface{}
t = "hello"

switch t := t.(type) {
case string:
    fmt.Printf("string: %s\n", t)
case bool:
    fmt.Printf("boolean: %v\n", t)
case int:
    fmt.Printf("integer: %d\n", t)
default:
    fmt.Printf("unexpected: %T\n", t)
}
Enter fullscreen mode Exit fullscreen mode

如果运行这个,我们可以验证我们有一个string类型

$ go run main.go
string: hello
Enter fullscreen mode Exit fullscreen mode

特性

让我们讨论一下接口的一些属性。

零值

接口的零值是nil

package main

import "fmt"

type MyInterface interface {
    Method()
}

func main() {
    var i MyInterface

    fmt.Println(i) // Output: <nil>
}
Enter fullscreen mode Exit fullscreen mode

嵌入

我们可以嵌入像结构一样的接口。

例如

type interface1 interface {
    Method1()
}

type interface2 interface {
    Method2()
}

type interface3 interface {
    interface1
    interface2
}
Enter fullscreen mode Exit fullscreen mode

价值观

接口值是可比较的

package main

import "fmt"

type MyInterface interface {
    Method()
}

type MyType struct{}

func (MyType) Method() {}

func main() {
    t := MyType{}
    var i MyInterface = MyType{}

    fmt.Println(t == i)
}
Enter fullscreen mode Exit fullscreen mode

接口值

从本质上讲,接口值可以被认为是由值和具体类型组成的元组。

package main

import "fmt"

type MyInterface interface {
    Method()
}

type MyType struct {
    property int
}

func (MyType) Method() {}

func main() {
    var i MyInterface

    i = MyType{10}

    fmt.Printf("(%v, %T)\n", i, i) // Output: ({10}, main.MyType)
}
Enter fullscreen mode Exit fullscreen mode

至此,我们介绍了 Go 中的接口。

这是一个非常强大的功能,但请记住“界面越大,抽象越弱” - Rob Pike。

错误

在本教程中,我们来讨论错误处理。

请注意,我说的是错误而不是异常,因为 Go 中没有异常处理。

相反,我们可以只返回一个内置error类型,即接口类型。

type error interface {
    Error() string
}
Enter fullscreen mode Exit fullscreen mode

我们很快就会回到这个问题。首先,我们来了解一下基础知识。

因此,让我们声明一个简单的Divide函数,顾名思义,它将整数a除以b

func Divide(a, b int) int {
    return a/b
}
Enter fullscreen mode Exit fullscreen mode

太好了。现在,我们想返回一个错误,比如说,为了防止除以零。这就引出了错误构造。

构造错误

有多种方法可以做到这一点,但我们将讨论最常见的两种。

errors包裹

第一种是使用包New提供的功能errors

package main

import "errors"

func main() {}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }

    return a/b, nil
}
Enter fullscreen mode Exit fullscreen mode

注意,我们如何将错误与结果一起返回。如果没有错误,我们直接返回nil错误零值,毕竟它是一个接口。

但是我们该如何处理呢?因此,让我们在函数Divide中调用该函数main

package main

import (
    "errors"
    "fmt"
)

func main() {
    result, err := Divide(4, 0)

    if err != nil {
        fmt.Println(err)
        // Do something with the error
        return
    }

    fmt.Println(result)
    // Use the result
}

func Divide(a, b int) (int, error) {...}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
cannot divide by zero
Enter fullscreen mode Exit fullscreen mode

如你所见,我们只是检查错误是否存在,nil并据此构建逻辑。这在 Go 中被认为是非常惯用的,你会看到它被广泛使用。

构建错误的另一种方法是使用fmt.Errorf函数。

此函数类似于fmt.Sprintf并让我们格式化我们的错误,但它不是返回字符串,而是返回错误。

它通常用于为我们的错误添加一些背景或细节。

...
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide %d by zero", a)
    }

    return a/b, nil
}
Enter fullscreen mode Exit fullscreen mode

它的工作原理应该类似

$ go run main.go
cannot divide 4 by zero
Enter fullscreen mode Exit fullscreen mode

哨兵错误

Go 中另一项重要技术是定义预期错误,以便可以在代码的其他部分显式地检查它们。这些错误有时被称为标记错误 (sentinel error)。

package main

import (
    "errors"
    "fmt"
)

var ErrDivideByZero = errors.New("cannot divide by zero")

func main() {...}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivideByZero
    }

    return a/b, nil
}
Enter fullscreen mode Exit fullscreen mode

在 Go 中,在变量前添加 被认为是惯例Err

例如,ErrNotFound

但重点是什么?

因此,当遇到某种错误时我们需要执行不同的代码分支时,这会变得很有用。

例如,现在我们可以使用errors.Is函数明确检查发生了哪个错误

package main

import (
    "errors"
    "fmt"
)

func main() {
    result, err := Divide(4, 0)

    if err != nil {
        switch {
    case errors.Is(err, ErrDivideByZero):
        fmt.Println(err)
                // Do something with the error
    default:
        fmt.Println("no idea!")
    }

        return
    }

    fmt.Println(result)
    // Use the result
}

func Divide(a, b int) (int, error) {...}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
cannot divide by zero
Enter fullscreen mode Exit fullscreen mode

自定义错误

此策略涵盖了大多数错误处理用例。但有时我们需要额外的功能,例如在错误中使用动态值。

之前我们看到这error只是一个接口。所以基本上,任何东西都可以是,error只要它实现了Error()返回字符串形式的错误消息的方法。

因此,让我们定义DivisionError包含错误代码和消息的自定义结构。

package main

import (
    "errors"
    "fmt"
)

type DivisionError struct {
    Code int
    Msg  string
}

func (d DivisionError) Error() string {
    return fmt.Sprintf("code %d: %s", d.Code, d.Msg)
}

func main() {...}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, DivisionError{
            Code: 2000,
            Msg:  "cannot divide by zero",
        }
    }

    return a/b, nil
}
Enter fullscreen mode Exit fullscreen mode

在这里,我们将使用errors.As而不是errors.Is函数将错误转换为正确的类型。

func main() {
    result, err := Divide(4, 0)

    if err != nil {
        var divErr DivisionError

        switch {
        case errors.As(err, &divErr):
            fmt.Println(divErr)
            // Do something with the error
        default:
            fmt.Println("no idea!")
        }

        return
    }

    fmt.Println(result)
    // Use the result
}

func Divide(a, b int) (int, error) {...}
Enter fullscreen mode Exit fullscreen mode
$ go run man.go
code 2000: cannot divide by zero
Enter fullscreen mode Exit fullscreen mode

errors.Is但是和之间有什么区别errors.As

不同之处在于,此函数检查错误是否具有特定类型,而不像Is(),它检查它是否是特定的错误对象。

我们也可以使用类型断言,但这不是首选

func main() {
    result, err := Divide(4, 0)

    if e, ok := err.(DivisionError); ok {
        fmt.Println(e.Code, e.Msg) // Output: 2000 cannot divide by zero
        return
    }

    fmt.Println(result)
}
Enter fullscreen mode Exit fullscreen mode

最后,我想说的是,Go 中的错误处理与其他语言的传统做法截然不同try/catch。但它非常强大,因为它鼓励开发人员以显式的方式实际处理错误,从而提高了代码的可读性。

希望本教程能帮助您了解 Go 中的错误以及如何处理它们。下期再见!

恐慌与恢复

前面我们了解到,Go 程序中处理异常情况的惯用方法是使用错误。虽然错误在大多数情况下已经足够,但在某些情况下程序无法继续运行。

在这些情况下,我们可以使用内置panic函数。

func panic(interface{})
Enter fullscreen mode Exit fullscreen mode

Panic 是一个内置函数,用于停止当前 的正常执行goroutine。当一个函数调用 时panic,该函数的正常执行将立即停止,并将控制权交还给调用者。此过程将重复进行,直到程序退出并返回 panic 消息和堆栈跟踪。

注意:我们将goroutines在课程后面讨论

让我们看看如何使用该panic函数

package main

func main() {
    WillPanic()
}

func WillPanic() {
    panic("Woah")
}
Enter fullscreen mode Exit fullscreen mode

如果我们运行这个,我们可以看到panic实际效果

$ go run main.go
panic: Woah

goroutine 1 [running]:
main.WillPanic(...)
        .../main.go:8
main.main()
        .../main.go:4 +0x38
exit status 2
Enter fullscreen mode Exit fullscreen mode

正如预期的那样,我们的程序打印了恐慌消息,然后是堆栈跟踪,然后终止。

那么问题来了,当意外的恐慌发生时该怎么办?

嗯,可以使用内置recover函数和defer关键字重新控制崩溃的程序。

func recover() interface{}
Enter fullscreen mode Exit fullscreen mode

让我们尝试创建一个handlePanic函数来做个例子。然后我们可以使用defer

package main

import "fmt"

func main() {
    WillPanic()
}

func handlePanic() {
    data := recover()
    fmt.Println("Recovered:", data)
}

func WillPanic() {
    defer handlePanic()

    panic("Woah")
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Recovered: Woah
Enter fullscreen mode Exit fullscreen mode

我们可以看到,我们的恐慌已经恢复,现在我们的程序可以继续执行。

最后,我想提一下,这panic与其他语言中的惯用法recover类似try/catch。一个重要的因素是,我们应该避免恐慌,并尽可能地恢复和使用错误。

如果是这样,那么这就引出了一个问题,我们应该何时使用panic

有两个有效用例panic

  • 不可恢复的错误

这可能是程序无法简单地继续执行的情况。

例如,读取对于启动程序很重要的配置文件,因为如果文件读取本身失败,则没有其他可做的事情。

  • 开发人员错误

这是最常见的情况。

例如,当值是时取消引用指针nil将导致恐慌。

希望本教程能帮助您理解如何在 Go 中使用panicrecover。下期再见。

测试

在本教程中,我们将讨论 Go 中的测试。那么,让我们从一个简单的例子开始。

我们创建了一个math包含Add函数的包,顾名思义,该函数可以将两个整数相加。

package math

func Add(a, b int) int {
    return a + b
}
Enter fullscreen mode Exit fullscreen mode

main它在我们的包中被这样使用

package main

import (
    "example/math"
    "fmt"
)

func main() {
    result := math.Add(2, 2)
    fmt.Println(result)
}

Enter fullscreen mode Exit fullscreen mode

如果我们运行这个,我们应该看到结果

$ go run main.go
4
Enter fullscreen mode Exit fullscreen mode

现在,我们要测试我们的Add函数。因此,在 Go 中,我们声明_test文件名带有后缀的测试文件。因此add.go,我们将创建一个测试,如下所示:add_test.go

我们的项目结构应该是这样的。

.
├── go.mod
├── main.go
└── math
    ├── add.go
    └── add_test.go
Enter fullscreen mode Exit fullscreen mode

我们将从使用一个math_test包开始,并testing从标准库中导入该包。没错!与许多其他语言不同,Go 内置了测试功能。

但是等等...为什么我们需要使用它math_test作为我们的包,我们不能只使用相同的math包吗?

是的,如果我们愿意,我们可以在同一个包中编写测试,但我个人认为在单独的包中执行此操作有助于我们以更加解耦的方式编写测试。

现在我们可以创建TestAdd函数了。它将接受一个类型的参数,testing.T该类型将为我们提供有用的方法。

package math_test

import "testing"

func TestAdd(t *testing.T) {}
Enter fullscreen mode Exit fullscreen mode

在添加任何测试逻辑之前,我们先尝试运行一下。不过这次我们不能使用go run命令,而是使用go test命令。

$ go test ./math
ok      example/math 0.429s
Enter fullscreen mode Exit fullscreen mode

在这里,我们将使用我们的包名称,math但我们也可以使用相对路径./...来测试所有包。

$ go test ./...
?       example [no test files]
ok      example/math 0.348s
Enter fullscreen mode Exit fullscreen mode

如果 Go 在包中找不到任何测试,它就会通知我们。

好了,让我们来写一些测试代码。为此,我们将结果与预期值进行检查,如果它们不匹配,我们可以使用该t.Fail方法来使测试失败。

package math_test

import "testing"

func TestAdd(t *testing.T) {
    got := math.Add(1, 1)
    expected := 2

    if got != expected {
        t.Fail()
    }
}
Enter fullscreen mode Exit fullscreen mode

太好了!我们的测试似乎通过了。

$ go test math
ok      example/math    0.412s
Enter fullscreen mode Exit fullscreen mode

我们还看看如果测试失败会发生什么,因此,我们可以改变我们的预期结果

package math_test

import "testing"

func TestAdd(t *testing.T) {
    got := math.Add(1, 1)
    expected := 3

    if got != expected {
        t.Fail()
    }
}
Enter fullscreen mode Exit fullscreen mode
$ go test ./math
ok      example/math    (cached)
Enter fullscreen mode Exit fullscreen mode

如果您看到此信息,请不要担心。为了优化,我们的测试已缓存。我们可以使用该go clean命令清除缓存,然后重新运行测试。

$ go clean -testcache
$ go test ./math
--- FAIL: TestAdd (0.00s)
FAIL
FAIL    example/math    0.354s
FAIL
Enter fullscreen mode Exit fullscreen mode

测试失败的情况就是这样的。

表驱动测试

这让我们开始讨论表驱动测试。但它到底是什么呢?

之前我们定义了函数参数和预期变量,并将它们进行比较以确定测试是否通过。但如果我们将所有这些定义在一个切片中并对其进行迭代呢?这将使我们的测试更加灵活,并帮助我们轻松运行多个用例。

别担心,我们会通过例子来学习。所以我们先从定义addTestCase结构体开始。

package math_test

import (
    "example/math"
    "testing"
)

type addTestCase struct {
    a, b, expected int
}

var testCases = []addTestCase{
    {1, 1, 3},
    {25, 25, 50},
    {2, 1, 3},
    {1, 10, 11},
}

func TestAdd(t *testing.T) {

    for _, tc := range testCases {
        got := math.Add(tc.a, tc.b)

        if got != tc.expected {
            t.Errorf("Expected %d but got %d", tc.expected, got)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

注意,我们是addTestCase用小写字母声明的。没错,我们不想导出它,因为它在测试逻辑之外没有用处。

让我们运行测试

$ go run main.go
--- FAIL: TestAdd (0.00s)
    add_test.go:25: Expected 3 but got 2
FAIL
FAIL    example/math    0.334s
FAIL
Enter fullscreen mode Exit fullscreen mode

好像我们的测试失败了,让我们通过更新测试用例来修复它们。

var testCases = []addTestCase{
    {1, 1, 2},
    {25, 25, 50},
    {2, 1, 3},
    {1, 10, 11},
}
Enter fullscreen mode Exit fullscreen mode

完美,正在运行!

$ go run main.go
ok      example/math    0.589s
Enter fullscreen mode Exit fullscreen mode

代码覆盖率

最后,我们来谈谈代码覆盖率。编写测试时,了解测试覆盖了多少实际代码通常很重要。这通常称为代码覆盖率。

要计算和导出测试的覆盖率,我们可以简单地使用命令-coverprofile中的参数go test

$ go test ./math -coverprofile=coverage.out
ok      example/math    0.385s  coverage: 100.0% of statements
Enter fullscreen mode Exit fullscreen mode

看来我们的覆盖范围很广。我们再用go tool cover命令检查一下报告,它会给出详细的报告。

$ go tool cover -html=coverage.out
Enter fullscreen mode Exit fullscreen mode

覆盖范围

我们看到,这是一种更易读的格式。最重要的是,它内置于标准工具中。

模糊测试

最后,我们来看看 Go 1.18 版本中引入的模糊测试。

模糊测试是一种自动化测试,它不断操纵程序的输入来查找错误。

Go fuzzing 使用覆盖指导来智能地遍历被模糊测试的代码,以查找故障并向用户报告。

由于模糊测试可以发现人类经常忽略的边缘情况,因此它对于查找错误和安全漏洞特别有价值。

让我们尝试一个例子,

func FuzzTestAdd(f *testing.F) {
    f.Fuzz(func(t *testing.T, a, b int) {
        math.Add(a , b)
    })
}
Enter fullscreen mode Exit fullscreen mode

运行这个程序,我们会看到它会自动创建测试用例。由于我们的Add功能非常简单,测试应该能够通过。

$ go test -fuzz FuzzTestAdd example/math
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok      foo 12.692s
Enter fullscreen mode Exit fullscreen mode

但是如果我们用随机边缘情况更新我们的函数,这样当大于时Add程序就会恐慌b + 10a

func Add(a, b int) int {
    if a > b + 10 {
        panic("B must be greater than A")
    }

    return a + b
}
Enter fullscreen mode Exit fullscreen mode

如果我们重新运行测试,模糊测试就会捕获这个边缘情况。

$ go test -fuzz FuzzTestAdd example/math
warning: starting with empty corpus
fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0)
fuzz: elapsed: 0s, execs: 1 (25/sec), new interesting: 0 (total: 0)
--- FAIL: FuzzTestAdd (0.04s)
    --- FAIL: FuzzTestAdd (0.00s)
        testing.go:1349: panic: B is greater than A
Enter fullscreen mode Exit fullscreen mode

我认为这是 Go 1.18 的一个非常酷的功能。您可以从官方 Go 博客中了解有关模糊测试的更多信息。

完美,本教程基本就到这里。下期再见!

泛型

在本节中,我们将了解泛型,这是 Go 1.18 版本发布的一项备受期待的功能

什么是泛型?

泛型指的是参数化类型。简而言之,泛型允许程序员编写代码,因为类型并非立即生效,所以可以稍后再指定类型。

让我们举一个例子来更好地理解这一点。

在我们的例子中,我们为不同类型的对象(例如、和)提供了简单的求和函数。int由于float64Gostring不允许方法重写,我们通常必须创建新函数。

package main

import "fmt"

func sumInt(a, b int) int {
    return a + b
}

func sumFloat(a, b float64) float64 {
    return a + b
}

func sumString(a, b string) string {
    return a + b
}

func main() {
    fmt.Println(sumInt(1, 2))
    fmt.Println(sumFloat(4.0, 2.0))
    fmt.Println(sumString("a", "b"))
}
Enter fullscreen mode Exit fullscreen mode

我们可以看到,除了类型之外,这些函数非常相似。

让我们看看如何定义通用函数。

func fnName[T constraint]() {
    ...
}
Enter fullscreen mode Exit fullscreen mode

这里,T是我们的类型参数,并且**constraint将是允许任何类型**实现该接口**的接口。

我知道这有点让人困惑。那么,让我们开始构建我们的通用sum函数吧。

在这里我们将使用T空作为我们的类型参数interface{}作为我们的约束。

func sum[T interface{}](a, b T) T {
    fmt.Println(a, b)
}
Enter fullscreen mode Exit fullscreen mode

此外,从 Go 1.18 开始我们可以使用any与空接口几乎等同的东西。

func sum[T any](a, b T) T {
    fmt.Println(a, b)
}
Enter fullscreen mode Exit fullscreen mode

使用类型参数时需要传递类型参数,这会使我们的代码变得冗长。

sum[int](1, 2) // explicit type argument
sum[float64](4.0, 2.0)
sum[string]("a", "b")
Enter fullscreen mode Exit fullscreen mode

幸运的是,Go 1.18 带有类型推断,它可以帮助我们编写调用没有明确类型的泛型函数的代码。

sum(1, 2)
sum(4.0, 2.0)
sum("a", "b")
Enter fullscreen mode Exit fullscreen mode

让我们运行一下看看是否有效

$ go run main.go
1 2
4 2
a b
Enter fullscreen mode Exit fullscreen mode

现在让我们更新sum函数以添加我们的变量。

func sum[T any](a, b T) T {
    return a + b
}
Enter fullscreen mode Exit fullscreen mode
fmt.Println(sum(1, 2))
fmt.Println(sum(4.0, 2.0))
fmt.Println(sum("a", "b"))
Enter fullscreen mode Exit fullscreen mode

但是现在如果我们运行它,我们将收到一个错误,即运算符+未在约束中定义。

$ go run main.go
./main.go:6:9: invalid operation: operator + not defined on a (variable of type T constrained by any)
Enter fullscreen mode Exit fullscreen mode

虽然类型约束any通常有效,但它不支持运算符。

因此,让我们使用接口定义我们自己的自定义约束。我们的接口应该定义一个包含intfloat和 的类型集string

排版

SumConstraint我们的界面是这样的

type SumConstraint interface {
    int | float64 | string
}

func sum[T SumConstraint](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(sum(1, 2))
    fmt.Println(sum(4.0, 2.0))
    fmt.Println(sum("a", "b"))
}
Enter fullscreen mode Exit fullscreen mode

这应该可以按预期工作

$ go run main.go
3
6
ab
Enter fullscreen mode Exit fullscreen mode

我们还可以使用constraints定义了一组与类型参数一起使用的有用约束的包。

约束包

为此,我们需要安装constraints

$ go get golang.org/x/exp/constraints
go: added golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
Enter fullscreen mode Exit fullscreen mode
import (
    "fmt"

    "golang.org/x/exp/constraints"
)

func sum[T constraints.Ordered](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(sum(1, 2))
    fmt.Println(sum(4.0, 2.0))
    fmt.Println(sum("a", "b"))
}
Enter fullscreen mode Exit fullscreen mode

这里我们使用了Ordered约束。

type Ordered interface {
    Integer | Float | ~string
}
Enter fullscreen mode Exit fullscreen mode

~是添加到 Go 的新标记,表达式~string表示所有类型的集合,其底层类型为string

它仍然按预期工作

$ go run main.go
3
6
ab
Enter fullscreen mode Exit fullscreen mode

泛型是一个令人惊叹的特性,因为它允许编写抽象函数,在某些情况下可以大大减少代码重复。

何时使用泛型

那么,什么时候使用泛型呢?我们可以以以下用例为例

  • 对数组、切片、映射和通道进行操作的函数
  • 通用数据结构,例如堆栈或链表
  • 减少代码重复

最后,我要补充一点,虽然泛型对于语言来说是一个很好的补充,但是应该谨慎使用。

并且,建议从简单的开始,只有在我们编写了至少 2 或 3 次非常相似的代码后才编写通用代码。

完美,关于泛型的讨论到此结束。下篇教程再见!

并发

在本课中,我们将学习并发性,这是 Go 最强大的功能之一。

那么我们首先要问什么是“并发”

什么是并发

并发,顾名思义,就是将计算机程序或算法分解为各个部分,每个部分都可以独立执行的能力。

并发程序的最终结果与顺序执行的程序的最终结果相同。

使用并发,我们能够在更短的时间内获得相同的结果,从而提高程序的整体性能和效率。

并发与并行

并发与并行

很多人将并发与并行混淆,因为它们都暗示同时执行代码,但它们是两个完全不同的概念。

并发是指同时运行和管理多个计算的任务。而并行是指同时运行多个计算的任务。

Rob Pike 的一句简单引言几乎概括了这一点。

并发是指同时处理大量事务。并行是指同时执行大量任务。

但 Go 中的并发不仅仅是语法。为了充分利用 Go 的强大功能,我们首先需要了解 Go 如何处理代码的并发执行。Go 依赖于一种称为 CSP(通信顺序进程)的并发模型。

通信顺序进程(CSP)

通信顺序进程(CSP)是由 Tony Hoare 于 1978 年提出的一个描述并发进程间交互的模型,它在计算机科学领域,尤其是在并发领域取得了突破性进展。

Go 和 Erlang 等语言深受通信顺序进程 (CSP) 概念的启发。

并发很难,但 CSP 使我们能够更好地构建并发代码,并提供了一个更简单的并发思考模型。在这里,进程是独立的,它们通过共享通道进行通信。

中央处理器

我们将在课程后面学习 Golang 如何使用 goroutines 和通道实现它。

基本概念

现在让我们熟悉一些基本的并发概念

数据竞赛

当进程必须同时访问同一个变量时,就会发生数据争用。

例如,一个进程读取而另一个进程同时写入完全相同的变量。

竞争条件

当事件的时间或顺序影响一段代码的正确性时,就会出现竞争条件。

死锁

当所有进程因相互等待而被阻塞,程序无法继续执行时,就会发生死锁。

科夫曼条件

有四个条件,称为科夫曼条件,所有条件都必须满足才会发生死锁。

  • 互斥

并发进程在任何时候都持有至少一个资源,使其不可共享。

在下图中,资源 1 有一个实例,并且仅由进程 1 持有。

互斥

  • 等待

并发进程持有资源并正在等待额外的资源。

在下图中,进程 2 持有资源 2 和资源 3,并请求进程 1 持有的资源 1。

等待

  • 不抢占

并发进程持有的资源不能被系统拿走,只能由持有该资源的进程释放。

如下图所示,进程 2 不能从进程 1 手中抢占资源 1,只有进程 1 执行完成后主动放弃资源 1 时,资源 1 才会被释放。

不抢占

  • 循环等待

一个进程等待第二个进程持有的资源,第二个进程又等待第三个进程持有的资源,以此类推,直到最后一个进程等待第一个进程持有的资源。如此循环下去。

在下图中,进程 1 被分配了资源 2,并且它正在请求资源 1。同样,进程 2 被分配了资源 1,并且它正在请求资源 2。这形成了一个循环等待循环。

循环等待

饥饿

当一个进程被剥夺了必要的资源并且无法完成其功能时,就会发生饥饿。

资源匮乏可能是由于进程死锁或低效的调度算法造成的。为了解决资源匮乏问题,我们需要采用更合理的资源分配算法,确保每个进程都能获得公平的资源份额。

至此,我们结束了关于 Go 并发基础知识的讨论。下节课再见,我们将学习 Channel 和 Goroutine。

Goroutines

在本课中,我们将学习 Goroutines。

但在我们开始讨论之前,我想分享一个重要的围棋谚语。

“不要通过共享内存来沟通,而要通过沟通来共享内存。” - Rob Pike

什么是 goroutine?

goroutine是一个轻量级的执行线程由 Go 运行时管理,本质上让我们以同步的方式编写异步代码。

重要的是要知道它们不是实际的 OS 线程,并且主函数本身作为 goroutine 运行。

通过使用协作式调度的 Go 运行时调度程序,单个线程可以运行数千个 Goroutine。这意味着,如果当前 Goroutine 被阻塞或已完成,调度程序会将其他 Goroutine 移至另一个 OS 线程。因此,我们实现了高效的调度,因为没有 Goroutine 会被永久阻塞。

我们只需使用go关键字就可以将任何函数转换为 goroutine

go fn(x, y, z)
Enter fullscreen mode Exit fullscreen mode

在编写任何代码之前,有必要简要讨论一下 fork-join 模型。

Fork-Join 模型

Go 在 goroutine 背后采用了 fork-join 模型的并发思想。fork-join 模型的本质是,子进程从其父进程中分离出来,与父进程并发运行。执行完成后,子进程会重新合并回父进程。子进程重新加入父进程的点称为连接点

分叉-连接

现在让我们编写一些代码并创建我们自己的 goroutine

package main

import "fmt"

func speak(arg string) {
    fmt.Println(arg)
}

func main() {
    go speak("Hello World")
}
Enter fullscreen mode Exit fullscreen mode

这里的speak函数调用以关键字为前缀go。这将允许它作为单独的 Goroutine 运行。就这样,我们创建了第一个 Goroutine。就这么简单!

太好了,我们来运行一下

$ go run main.go

Enter fullscreen mode Exit fullscreen mode

有趣的是,我们的程序似乎没有完全运行,因为它缺少一些输出。这是因为我们的主 Goroutine 退出了,没有等待我们创建的 Goroutine。

如果我们使用该函数让程序等待会怎么样time.Sleep

func main() {
    ...
    time.Sleep(1 * time.Second)
}
Enter fullscreen mode Exit fullscreen mode

现在如果我们运行这个

$ go run main.go
Hello World
Enter fullscreen mode Exit fullscreen mode

好了,我们现在可以看到完整的输出。

好吧,虽然这样可行,但效果并不理想。那么,我们该如何改进呢?

嗯,使用 goroutine 最棘手的部分是知道它们何时停止。重要的是要知道 goroutine 运行在相同的地址空间中,因此对共享内存的访问必须同步。

这将引出渠道问题,我们将在下一篇中讨论。

频道

在本课中,我们将学习渠道。

那么什么是渠道?

嗯,简单定义一下。

通道是 Goroutine 之间的通信管道。数据以相同的顺序从一端进入,从另一端出来,直到通道关闭。

渠道

正如我们之前所了解的,Go 中的通道基于通信顺序进程(CSP)。

创建频道

现在我们了解了什么是通道,让我们看看如何声明它们

var ch chan T
Enter fullscreen mode Exit fullscreen mode

在这里,我们在我们的类型T(即我们想要发送和接收的值的数据类型)前加上chan代表通道的关键字作为前缀。

c让我们尝试打印类型通道的值string

func main() {
    var ch chan string

    fmt.Println(c)
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
<nil>
Enter fullscreen mode Exit fullscreen mode

正如我们所见,通道的零值是nil,如果我们尝试通过通道发送数据,我们的程序就会崩溃。

因此与切片类似,我们可以使用内置make函数初始化我们的通道。

func main() {
    ch := make(chan string)

    fmt.Println(c)
}
Enter fullscreen mode Exit fullscreen mode

如果我们运行这个程序,我们可以看到我们的频道已经初始化了。

$ go run main.go
0x1400010e060
Enter fullscreen mode Exit fullscreen mode

发送和接收数据

现在我们对通道有了基本的了解,让我们使用通道实现我们之前的示例,以了解如何使用它们在我们的 goroutine 之间进行通信。

package main

import "fmt"

func speak(arg string, ch chan string) {
    ch <- arg // Send
}

func main() {
    ch := make(chan string)

    go speak("Hello World", ch)

    data := <-ch // Receive
    fmt.Println(data)
}
Enter fullscreen mode Exit fullscreen mode

注意我们如何使用语法发送数据channel<-data以及如何接收数据data := <-channel

如果我们运行这个

$ go run main.go
Hello World
Enter fullscreen mode Exit fullscreen mode

完美,我们的程序按预期运行。

缓冲通道

我们还有缓冲通道,它接受有限数量的值,而没有相应的接收器。

缓冲通道

可以使用函数的第二个参数指定缓冲区的长度容量make

func main() {
    ch := make(chan string, 2)

    go speak("Hello World", ch)
    go speak("Hi again", ch)

    data1 := <-ch
    fmt.Println(data1)

    data2 := <-ch
    fmt.Println(data2)
}
Enter fullscreen mode Exit fullscreen mode

因为这个通道是缓冲的,所以我们可以将这些值发送到通道中,而无需相应的并发接收。

默认情况下,通道是无缓冲的,容量为 0,因此我们省略了make函数的第二个参数。

接下来,我们有定向渠道。

定向通道

当使用通道作为函数参数时,我们可以指定通道是仅用于发送值还是仅用于接收值。这提高了程序的类型安全性,因为默认情况下,通道可以同时发送和接收值。

定向通道

在我们的示例中,我们可以更新speak函数的第二个参数,以便它只能发送一个值。

func speak(arg string, ch chan<- string) {
    ch <- arg // Send Only
}
Enter fullscreen mode Exit fullscreen mode

这里,chan<-只能用于发送值,如果我们尝试接收值,就会发生恐慌。

关闭通道

另外,就像任何其他资源一样,一旦我们完成了通道的操作,就需要关闭它。这可以使用内置close函数来实现。

在这里,我们可以将我们的通道传递给close函数。

func main() {
    ch := make(chan string, 2)

    go speak("Hello World", ch)
    go speak("Hi again", ch)

    data1 := <-ch
    fmt.Println(data1)

    data2 := <-ch
    fmt.Println(data2)

    close(ch)
}
Enter fullscreen mode Exit fullscreen mode

可选地,接收者可以通过向接收表达式分配第二个参数来测试通道是否已关闭。

func main() {
    ch := make(chan string, 2)

    go speak("Hello World", ch)
    go speak("Hi again", ch)

    data1 := <-ch
    fmt.Println(data1)

    data2, ok := <-ch
    fmt.Println(data2, ok)

    close(ch)
}
Enter fullscreen mode Exit fullscreen mode

如果ok是,false则表示没有更多值可供接收,并且通道已关闭。

在某种程度上这类似于我们检查地图中某个键是否存在的方式。

特性

最后,我们来讨论一下渠道的一些属性

  • 发送到 nil 通道永远阻塞
var c chan string
c <- "Hello, World!" // Panic: all goroutines are asleep - deadlock!
Enter fullscreen mode Exit fullscreen mode
  • 从零通道接收永远阻塞
var c chan string
fmt.Println(<-c) // Panic: all goroutines are asleep - deadlock!
Enter fullscreen mode Exit fullscreen mode
  • 向已关闭的通道发送消息时发生恐慌
var c = make(chan string, 1)
c <- "Hello, World!"
close(c)
c <- "Hello, Panic!" // Panic: send on closed channel
Enter fullscreen mode Exit fullscreen mode
  • 从已关闭的通道接收数据会立即返回零值
var c = make(chan int, 2)
c <- 5
c <- 4
close(c)
for i := 0; i < 4; i++ {
    fmt.Printf("%d ", <-c) // Output: 5 4 0 0
}
Enter fullscreen mode Exit fullscreen mode
  • 范围覆盖渠道

我们还可以使用forandrange来迭代从通道接收的值

package main

import "fmt"

func main() {
    ch := make(chan string, 2)

    ch <- "Hello"
    ch <- "World"

    close(ch)

    for data := range ch {
        fmt.Println(data)
    }
}
Enter fullscreen mode Exit fullscreen mode

至此,我们学习了 Go 中 goroutine 和 channel 的工作原理。希望这些内容对您有所帮助。下篇教程再见。

选择

在本教程中,我们将学习selectGo 中的语句

select语句阻止代码并同时等待多个通道操作。

Aselect会阻塞,直到其中一个 case 可以运行,然后执行该 case。如果有多个 case 就绪,它会随机选择一个。

package main

import (
    "fmt"
    "time"
)

func main() {
    one := make(chan string)
    two := make(chan string)

    go func() {
        time.Sleep(time.Second * 2)
        one <- "One"
    }()

    go func() {
        time.Sleep(time.Second * 1)
        two <- "Two"
    }()

    select {
    case result := <-one:
        fmt.Println("Received:", result)
    case result := <-two:
        fmt.Println("Received:", result)
    }

    close(one)
    close(two)
}
Enter fullscreen mode Exit fullscreen mode

与 类似switchselect它也有一个默认用例,当其他用例都未就绪时,它会运行。这将帮助我们无阻塞地发送或接收数据。

func main() {
    one := make(chan string)
    two := make(chan string)

    for x := 0; x < 10; x++ {
        go func() {
            time.Sleep(time.Second * 2)
            one <- "One"
        }()

        go func() {
            time.Sleep(time.Second * 1)
            two <- "Two"
        }()
    }

    for x := 0; x < 10; x++ {
        select {
        case result := <-one:
            fmt.Println("Received:", result)
        case result := <-two:
            fmt.Println("Received:", result)
        default:
            fmt.Println("Default...")
            time.Sleep(200 * time.Millisecond)
        }
    }

    close(one)
    close(two)
}
Enter fullscreen mode Exit fullscreen mode

了解空白select {}会永远阻塞也很重要。

func main() {
    ...
    select {}

    close(one)
    close(two)
}
Enter fullscreen mode Exit fullscreen mode

这就是 Go 语言中 for 语句的全部内容select。下篇文章再见。

等待组

正如我们之前所了解的,goroutine 在同一个地址空间中运行,因此对共享内存的访问必须同步。

sync包提供了有用的原语。

因此在本教程中,我们将学习 waitgroups

基本上,WaitGroup帮助我们等待多个 goroutine 完成。

我们可以通过以下函数使用 WaitGroup:

  • Add(int)函数接受一个整数值,该整数值本质上是 waitgroup 需要等待的 goroutine 的数量。在执行 goroutine 之前必须调用此函数。
  • Done()在 goroutine 中调用函数来表示 goroutine 已成功执行。
  • Wait()函数会阻塞程序,直到所有由指定 goroutines从内部Add()调用为止。Done()

让我们举个例子

package main

import (
    "fmt"
    "sync"
)

func work() {
    fmt.Println("working...")
}

func main() {
    var wg sync.WaitGroup

    wg.Add(1)
    go func() {
        defer wg.Done()
        work()
    }()

    wg.Wait()
}
Enter fullscreen mode Exit fullscreen mode

如果我们运行这个,我们可以看到我们的程序按预期运行

$ go run main.go
working...
Enter fullscreen mode Exit fullscreen mode

我们也可以将权重组直接传递给函数。

func work(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("working...")
}

func main() {
    var wg sync.WaitGroup

    wg.Add(1)

    go work(&wg)

    wg.Wait()
}
Enter fullscreen mode Exit fullscreen mode

但需要注意的是,WaitGroup 不能被复制。如果要显式地将其传递给函数,则应该使用指针。因为这可能会影响我们的计数器,从而扰乱程序的逻辑。

我们还增加 goroutine 的数量并更新我们的 waitgroupAdd函数以等待 4 个 goroutine。

func main() {
    var wg sync.WaitGroup

    wg.Add(4)

    go work(&wg)
    go work(&wg)
    go work(&wg)
    go work(&wg)

    wg.Wait()
}
Enter fullscreen mode Exit fullscreen mode

正如预期的那样,我们所有的 goroutine 都已执行。

$ go run main.go
working...
working...
working...
working...
Enter fullscreen mode Exit fullscreen mode

本教程就是这样,下篇再见!

互斥体

在本教程中,我们将学习互斥锁。

什么是互斥锁?

互斥锁可防止其他进程在一个进程占用关键数据部分时进入该部分,以防止发生竞争条件。

什么是临界区?

因此,关键部分可以是一段不能由多个线程同时运行的代码,因为该代码包含共享资源。

我们可以通过以下函数使用 Mutex:

  • Lock()函数获取或持有锁
  • Unlock()函数释放锁。
  • TryLock()函数尝试锁定并报告是否成功。

让我们举个例子,我们将创建一个Counter结构并添加一个Update更新内部值的方法。

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    value int
}

func (c *Counter) Update(n int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Adding %d to %d\n", n, c.value)
    c.value += n
}

func main() {
    var wg sync.WaitGroup

    c := Counter{}

    wg.Add(4)

    go c.Update(10, &wg)
    go c.Update(-5, &wg)
    go c.Update(25, &wg)
    go c.Update(19, &wg)

    wg.Wait()
    fmt.Println(c.value)
}
Enter fullscreen mode Exit fullscreen mode

让我们运行它并看看会发生什么。

$ go run main.go
Adding -5 to 0
Adding 10 to 0
Adding 19 to 0
Adding 25 to 0
Result is 49
Enter fullscreen mode Exit fullscreen mode

这看起来不准确,好像我们的值总是零,但我们不知何故得到了正确的答案。

这是因为在我们的例子中,多个 goroutine 正在更新value变量。正如你一定猜到的,这并不理想。

这是 Mutex 的完美用例。那么,让我们首先在和函数之间使用sync.Mutex并包装临界区。Lock()Unlock()

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    m     sync.Mutex
    value int
}

func (c *Counter) Update(n int, wg *sync.WaitGroup) {
    c.m.Lock()
    defer wg.Done()
    fmt.Printf("Adding %d to %d\n", n, c.value)
    c.value += n
    c.m.Unlock()
}

func main() {
    var wg sync.WaitGroup

    c := Counter{}

    wg.Add(4)

    go c.Update(10, &wg)
    go c.Update(-5, &wg)
    go c.Update(25, &wg)
    go c.Update(19, &wg)

    wg.Wait()
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
Adding -5 to 0
Adding 19 to -5
Adding 25 to 14
Adding 10 to 39
Result is 49
Enter fullscreen mode Exit fullscreen mode

看起来我们解决了我们的问题并且输出看起来也是正确的。

包裹sync

到目前为止,我们已经讨论过了sync.WaitGroupsync.Mutex但是包中还有许多其他功能sync可以在编写并发代码时派上用场。

  • RWMutex代表“读者/写者互斥”,本质上与 相同Mutex,但它将锁赋予多个读进程或仅赋予一个写进程。它还使我们能够更好地控制内存。
  • Pool是临时对象的集合,可以被多个 goroutine 同时访问和保存。
  • Once是仅执行一次动作的对象。
  • Cond实现一个条件变量,指示正在等待事件或想要宣布事件的 goroutine。

后续步骤

希望这门课程能给你带来美好的学习体验。我很乐意听取你的反馈。祝你学习顺利!

鏂囩珷鏉ユ簮锛�https://dev.to/karanpratapsingh/learn-go-the-complete-course-plc
PREV
关于 JavaScript 中的日期对象你需要知道的一切
NEXT
JavaScript 中函数返回其他函数的强大之处