听说Golang也非常长的一段时间了, 也很清楚Go的很多优缺点, 很早就有点想去尝试这一个Google推出来的自动垃圾回收的C语言的. 不过由于种种原因一直拖到了现在
- 简洁的并发
- 高效率的编程
- 整齐的代码格式化
- 静态化编译,直接运行
正巧再前几天逛V2社区的时候看到了有人说微软都已经推出了Go语言的学习教程了, 然后我就点进去跟着走了一下, 顺便写写记录, 不然两天后又忘了
教程地址
安装 && 配置
由于众所周知的原因, Golang官方是一个不存在的404页面, 我们需要到国内的中文社区下载程序包然后直接安装就行 studygolang
代码编辑器这边就直接使用我一直再用的 Visual Studio Code, 不过这里安装Go的拓展后会提示去安装gopls
和go-outline
, 如果直接再VS code点击确认会一直安装失败, 需要我们手动去命令行安装
我这里是直接设置 goproxy 代理, 然后直接手动安装, 官方提示不要使用 -u.
1 | go get golang.org/x/tools/gopls@latest |
开始编码
Hello World
啥也别说, 先写一个hello world再说
新建一个main.go, 输入
1 | package main |
然后直接控制台直接运行
1 | $ go run main.go |
也可以打包为可执行的二进制文件
1 | $ go build main.go |
包、变量和函数
变量 & 常量
与 TypeScript 类似, Go中声明变量使用var
关键字, 常量使用const
关键字, 没有let
哈, Go中的var不存在js中的问题, 不需要let了
1 | var firstName string |
可以通过()来批量申明不同类型的变量
1 | var ( |
如果你决定初始化某个变量,则不需要指定其类型,因为当你使用具体值初始化该变量时,Go 会自动推断出其类型。
1 | var ( |
还可以通过另一种方式来声明和初始化变量。 此方法是在 Go 中执行此操作的最常见方法。 以下是我们使用的同一个示例说明:
1 | firstName, lastName := "John", "Doe" |
请注意,在定义变量名称后,需要在此处加入一个冒号等于号 (:=) 和相应的值。 使用冒号等于号时,要声明的变量必须是新变量。 如果使用冒号等于号并已经声明该变量,将不会对程序进行编译。
最终,你能在函数内使用冒号等于号。 在声明函数外的变量时,必须使用 var 关键字执行此操作。 如果你不熟悉函数,请不要担心。 我们会在后续单元中介绍函数。
如果声明了变量但未使用,Go 会抛出错误
了解基本数据类型
Go 是一种强类型语言。 这意味着你声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值。
Go 有四类数据类型:
基本类型:数字、字符串和布尔值
聚合类型:数组和结构
引用类型:指针、切片、映射、函数和通道
接口类型:接口
默认值
到目前为止,几乎每次声明变量时,都使用值对其进行了初始化。 但与在其他编程语言中不同的是,在 Go 中,如果你不对变量初始化,所有数据类型都有默认值。 此功能非常方便,因为在使用之前,你无需检查变量是否已初始化。
下面列出了我们目前浏览过类型的几个默认值:
int 类型的 0(及其所有子类型,如 int64)
float32 和 float64 类型的 +0.000000e+000
bool 类型的 false
string 类型的空值
类型转换
Go 中隐式强制转换不起作用,需要显式强制转换。 Go 提供了将一种数据类型转换为另一种数据类型的一些本机方法
1
2
3var integer16 int16 = 127
var integer32 int32 = 32767
println(int32(integer16) + integer32)Go 的另一种转换方法是使用
strconv
包。 例如,若要将 string 转换为 int,可以使用以下代码,反之亦然:
1 | package main |
下划线 (_) 用作变量的名称在 Go 中意味着我们不会使用该变量的值,而是要将其忽略。 否则,程序将不会进行编译,因为我们需要使用声明的所有变量。
创建函数
在 Go 中,函数允许你将一组可以从应用程序的其他部分调用的语句组合在一起。 你可以使用函数来组织代码并使其更易于阅读,而不是创建包含许多语句的程序。 更具可读性的代码也更易于维护。
main 函数
这也是常识了嘛, Go 中的所有可执行程序都具有main函数,因为它是程序的起点。 你的程序中只能有一个 main() 函数。 如果创建的是 Go 包,则无需编写 main() 函数。
自定义函数
下面是用于创建函数的语法:
1 | func name(parameters) (results) { |
请注意,使用 func 关键字来定义函数,然后为其指定名称。 在命名后,指定函数的参数列表。 你可以指定零个或多个参数。 你还可以定义函数的返回类型,该函数也可以是零个或多个。
1 | func calc(number1 string, number2 string) (sum int, mul int) { |
更改函数参数值(指针)
将值传递给函数时,该函数中的每个更改都不会影响调用方。 Go 是“按值传递”编程语言。 这意味着每次向函数传递值时,Go 都会使用该值并创建本地副本(内存中的新变量)。 在函数中对该变量所做的更改都不会影响你向函数发送的更改。
指针 是包含另一个变量的内存地址的变量。 当你发送指向某个函数的指针时,不会传递值,而是传递地址内存。 因此,对该变量所做的每个更改都会影响调用方。
在 Go 中,有两个运算符可用于处理指针:
& 运算符使用其后对象的地址。
- 运算符取消引用指针。 也就是说,你可以前往指针中包含的地址访问其中的对象。
1
2
3
4
5func updateName(name *string) {
*name = "David"
}
...
updateName(&firstName)
了解包
Go 包与其他编程语言中的库或模块类似。 你可以打包代码,并在其他位置重复使用它。 包的源代码可以分布在多个 .go 文件中。 到目前为止,我们已编写 main 包,并对其他本地包进行了一些引用。
main 包
你可能注意到,在 Go 中,甚至最直接的程序都是包的一部分。 通常情况下,默认包是 main 包,即目前为止一直使用的包。 如果程序是 main 包的一部分,Go 会生成二进制文件。 运行该文件时,它将调用 main() 函数。
换句话说,当你使用 main 包时,程序将生成独立的可执行文件。 但当程序非是 main 包的一部分时,Go 不会生成二进制文件。 它生成包存档文件(扩展名为 .a 的文件)。
创建包
在名为 calculator 的 $GOPATH/src 目录中创建新目录。 创建名为 sum.go 的文件。 树目录应如下列目录所示:
1 | src/ |
用包的名称初始化 sum.go 文件:
1 | package calculator |
你现在可以开始编写包的函数和变量。 不同于其他编程语言,Go 不会提供 public 或 private 关键字,以指示是否可以从包的内外部调用变量或函数。 但 Go 须遵循以下两个简单规则:
- 如需将某些内容设为专用内容,请以小写字母开始。
- 如需将某些内容设为公共内容,请以大写字母开始。
接下来,让我们将以下代码添加到我们要创建的计算器包:
1 | package calculator |
让我们看一下该代码中的一些事项:
只能从包内调用 logMessage 变量。
可以从任何位置访问 Version 变量。 建议你添加注释来描述此变量的用途。 (此描述适用于包的任何用户。)
只能从包内调用 internalSum 函数。
可以从任何位置访问 Sum 函数。 建议你添加注释来描述此函数的用途。
若要确认一切正常,可在 calculator 目录中运行 go build 命令。 如果执行此操作,请注意系统不会生成可执行的二进制文件。
创建模块
已将计算器功能组合到包中。 现在可以将包组合到模块中。 为什么? 包的模块指定了 Go 运行已组合代码所需的上下文。 此上下文信息包括编写代码时所用的 Go 版本。
此外,模块还有助于其他开发人员引用代码的特定版本,并更轻松地处理依赖项。 另一个优点是,我们的程序源代码无需严格存在于 $GOPATH/src 目录中。 如果释放该限制,则可以更方便地在其他项目中同时使用不同包版本。
因此,若要为 calculator 包创建模块,请在根目录 ($GOPATH/src/calculator) 中运行以下命令:
1 | go mod init github.com/myuser/calculator |
运行此命令后,github.com/myuser/calculator 就会变成包的名称。 在其他程序中,你将使用该名称进行引用。 命令还会创建一个名为 go.mod 的新文件。 最后,树目录现会如下列目录所示:
1 | src/ |
该文件的 go.mod 内容应该如下代码所示: (Go 版本可能不同。)
1 | module github.com/myuser/calculator |
若要在其他程序中引用此包,需要使用模块名称进行导入。 在这种情况下,其名称为 github.com/myuser/calculator。 现在,让我们看一下示例,了解如何使用此包。
引用本地包(模块)
现在,让我们先使用包。 我们将继续使用我们一直使用的示例应用程序。 这一次,我们将使用之前在 calculator 包中创建的函数,而不是 main 包中的 sum 函数。
树文件结构现应如下所示:
1 | src/ |
我们会将此代码用于 $GOPATH/src/helloworld/main.go 文件:
1 | package main |
请注意,import 语句使用所创建包的名称:calculator。 若要从该包调用 Sum 函数,你需要将包名称指定为 calculator.Sum。 最后,你现还可访问 Version 变量。 请按调用以下内容:calculator.Version。
如果立即尝试运行程序,它将不起任何作用。 你需要通知 Go,你会使用模块来引用其他包。 为此,请在 $GOPATH/src/helloworld 目录中运行以下命令:
1 | go mod init helloworld |
在上述命令中,helloworld 是项目名称。 此命令会创建一个新的 go.mod 文件,因此,树目录会如下所示:
1 | src/ |
如果打开 go.mod 文件,则应看到类似于下面代码的内容: (Go 版本可能不同。)
1 | module helloworld |
由于你引用的是该模块的本地副本,因此你需要通知 Go 不要使用远程位置。 因此,你需要手动修改 go.mod 文件,使其包含引用,如下所示:
1 | module helloworld |
replace 关键字指定使用本地目录,而不是模块的远程位置。 在这种情况下,由于 helloworld 和 calculator 程序在 $GOPATH/src 中,因此位置只能是 ../calculator。 如果模块源位于不同的位置,请在此处定义本地路径。
发布包
你可以轻松发布 Go 包。 只需公开提供包源代码即可实现。 大多数开发人员都使用 GitHub 公开发布包。 这就是为什么有时会在 import 语句中找到对 github.com 的引用。
例如,如果想要将 calculator 包发布到 GitHub 帐户,则需要创建一个名为 calculator 的存储库。 URL 应与下述网址类似:
1 | https://github.com/myuser/calculator |
你将通过标记存储库来对包进行版本化,如下所示:
1 | git tag v0.1.0 |
如果是想要使用你的包的开发人员(包括你自己)引用如下所述内容:
1 | import "github.com/myuser/calculator" |
让我们更详细地讨论如何引用第三方包。
引用外部(第三方)包
有时,程序需要引用其他开发人员编写的包。 你通常可以在 GitHub 上找到这些包。 不论你是要开发包(非 main 包)还是独立的程序(main 包),以下有关引用第三方包的说明均适用。
让我们添加对 rsc.io/quote 包的引用:
1 | package main |
如果使用 Visual Studio Code,则保存文件时将更新 go.mod 文件。 现在它的外观如下所示:
日后对第三方包的所有引用都需要包含在 go.mod 文件中。 运行或编译应用程序时,Go 将下载其所有依赖项。
流程控制
if & else
与其他编程语言不同的是,在 Go 中,你不需要在条件中使用括号。 else 子句可选。 但是,大括号仍然是必需的。 此外,为了减少行,Go 不支持三元 if 语句,因此每次都需要编写完整的 if 语句。
1 | if num := 1; num < 0 { |
Go中对代码格式要求很严格, 如果换行不符合标准会报错的, 而且if内申明的变量有作用域的限制, 在if条件外使用会出错
switch
Go 会执行 switch 语句的每个用例,直到找到条件的匹配项。case中可已判断多值。
1 | switch city { |
也可以在case中调用函数, switch中不做任何判断
再其他编程语言中case一般需要一个break
关键字结束查找, 但是再Go中进入case语句后会自动退出swtch。若要使逻辑进入到下一个紧邻的 case,请使用 fallthrough 关键字, 并且后续的case不在进行判断。
for, 循环
与 if 语句和 switch 语句一样,for 循环表达式不需要括号。 但是,大括号是必需的。
1 | sum := 0 |
Go中没有while
关键字, 但是我们可以通过for达到同样的效果
1 | for num != 5 { |
可以在 Go 中编写的另一种循环模式是无限循环。 在这种情况下,你不编写条件表达式,也不编写预处理语句或后处理语句, 而是采取退出循环的方式进行编写。 否则,逻辑永远都不会退出。 若要使逻辑退出循环,请使用 break
关键字。
1 | for { |
与其他语言一样, Go中也可以使用continue
关键字跳过当前循环
defer & panic & recover
1 | defer 语句会推迟函数(包括任何参数)的运行,直到包含 defer 语句的函数完成。 通常情况下,当你想要避免忘记任务(例如关闭文件或运行清理进程)时,可以推迟某个函数的运行。 |
1 | package main |
结构、数组、切片和映射
数组
1 | var a [3]int |
数组我觉得完全不需要多说啥了, 跟其他语言一样, Go中独特的是切片
切片
与数组更重要的区别是切片
的大小是动态的,不是固定的。
指针,指向基础数组可访问的第一个元素(并非一定是数组的第一个元素)。
长度,指示切片中的元素数目。
容量,显示切片开头与基础数组结束之间的元素数目。
切片申明方式与数组一致, 除了长度Go 支持切片运算符 s[i:j],其中:
s 表示数组。
i 表示指向它将使用的数组(或另一切片)的第一个元素的指针。
j 表示切片将使用的最后一个元素的位置。
Go 提供了内置函数 append(slice, element),便于你向切片添加元素。 你需要将要修改的切片和要追加的元素作为值发送给该函数。 然后,
append
函数会返回一个新的切片,你需要将其存储在变量中。
Go 没有内置函数用于从切片中删除元素。 可使用上述切片运算符 s[i:j] 来新建一个仅包含所需元素的切片。
Go 具有内置函数 copy(dst, src []Type) 用于创建切片的副本
1 | cities := string{"New York", "Paris", "Berlin", "Madrid"} |
映射
Go 中的映射是一个哈希表,是键值对的集合。 映射中所有的键都必须具有相同的类型,它们的值也是如此。 不过,可对键和值使用不同的类型。 例如,键可以是数字,值可以是字符串。 若要访问映射中的特定项,可引用该项的键。
1 | studentsAge := map[string]int{ |
结构
有时,你需要在一个结构中表示字段的集合。 例如,要编写工资核算程序时,需要使用员工数据结构。 在 Go 中,可使用结构将可能构成记录的不同字段组合在一起。
Go 中的结构也是一种数据结构,它可包含零个或多个任意类型的字段,并将它们表示为单个实体。
1 | /* 定义结构 */ |
每个字段分配值的顺序不重要。 此外,如果未指定任何其他字段的值,也并不重要。 Go 将根据字段数据类型分配默认值。
若要将结构编码为 JSON,请使用 json.Marshal 函数。 若要将 JSON 字符串解码为数据结构,请使用 json.Unmarshal 函数。
错误处理和日志记录
编写程序时,需要考虑程序失败的各种方式,并且需要管理失败。 无需让用户看到冗长而混乱的堆栈跟踪错误。 让他们看到有关错误的有意义的信息更好。 正如你所看到的,Go 具有 panic 和 recover 之类的内置函数来管理程序中的异常或意外行为。 但错误是已知的失败,你的程序应该可以处理它们。
1 | employee, err := getInformation(1000) |
方法和接口
Go 中的方法是一种特殊类型的函数,但存在一个简单的区别:你必须在函数名称之前加入一个额外的参数。 此附加参数称为 接收方。
如你希望分组函数并将其绑定到自定义类型,则方法非常有用。 Go 中的这一方法类似于在其他编程语言中创建类,因为它允许你实现面向对象编程 (OOP) 模型中的某些功能,例如嵌入、重载和封装。
1 | package main |
并发
通常,编写并发程序时最大的问题是在进程之间共享数据。 Go 采用不同于其他编程语言的通信方式,因为 Go 是通过 channel 来回传递数据的。 这意味着只有一个活动 (goroutine) 有权访问数据,设计上不存在争用条件。
1 |
|
Go 中的 channel 是 goroutine 之间的通信机制。 这就是为什么我们之前说过 Go 实现并发的方式是:“不是通过共享内存通信,而是通过通信共享内存。” 当你需要将值从一个 goroutine 发送到另一个 goroutine 时,将使用 channel。
一个 channel 可以执行两项操作:发送数据和接收数据。 若要指定 channel 具有的操作类型,需要使用 channel 运算符 <-。 此外,在 channel 中发送数据和接收数据属于阻止操作。 你一会儿就会明白为何如此。
如果希望 channel 仅发送数据,则必须在 channel 之后使用 <- 运算符。 如果希望 channel 接收数据,则必须在 channel 之前使用 <- 运算符,如下所示:
1 | ch <- x // sends (or write) x through channel ch |
有时需要在 goroutine 之间进行此类同步。 但是,有时你可能只需要实现并发,而不需要限制 goroutine 之间的通信方式。
1 | ch := make(chan string, 10) |
无缓冲 channel 与有缓冲 channel
现在,你可能想知道何时使用这两种类型。 这完全取决于你希望 goroutine 之间的通信如何进行。 无缓冲 channel 同步通信。 它们保证每次发送数据时,程序都会被阻止,直到有人从 channel 中读取数据。
相反,有缓冲 channel 将发送和接收操作解耦。 它们不会阻止程序,但你必须小心使用,因为可能最终会导致死锁(如前文所述)。 使用无缓冲 channel 时,可以控制可并发运行的 goroutine 的数量。 例如,你可能要对 API 进行调用,并且想要控制每秒执行的调用次数。 否则,你可能会被阻止。