Golang
Go 语言是一门需要编译才能运行的编程语言。
go build
编译,go run
会先编译再运行。
go.mod
/ go.sum
All the modules which are needed or to be used in the project are maintained in go.mod
file.
After running any package building command like go build, go test for the first time, it will install all the packages with specific versions. It will also create a go.sum
file which maintains the checksum so when you run the project again it will not install all packages again.
go.sum
is a generated file you don’t have to edit or modify this file.
Golang 语法
变量声明/定义:
// 变量声明与定义是放在一起的,也就是这个时候已经分配了内存空间了
var x int
make 是一个内置函数,专门用于分配和初始化 slice
(切片)、map
(映射) 和 channel
(通道) 三种数据类型。
make(type, length, capacity)
- type:要创建的类型,可以是 slice、map 或 channel。
- length(可选):数据结构的初始长度,适用于 slice 和 channel。
- capacity(可选):数据结构的容量,适用于 slice,如果不指定,容量默认等于长度。
if
In Golang
会先把分号前面的代码执行完,然后判断 exists 变量的值。
if config, exists := foo(); exists {
}
defer
In Golang
defer statements delay the execution of the function or method or an anonymous method until the nearby functions returns.
package main
import "fmt"
func mul(a1, a2 int) int {
res := a1 * a2
fmt.Println("Result: ", res)
return 0
}
func show(i int) {
fmt.Println("Hello!, GeeksforGeeks", i)
}
// Main function
func main() {
mul(23, 45)
defer mul(23, 56)
show(0)
show(1)
}
output:
Result: 1035
Hello!, GeeksforGeeks 0
Hello!, GeeksforGeeks 1
Result: 1288
可以看到上面 defer 的 mul
函数其实不是在 show()
函数返回时执行的,而是在大块函数(外面的 main() 函数返回的时候执行的)。
多个 defer 是 LIFO 的,也就是最先的 defer 最后执行。
Slice in Golang
Go 语言切片的长度和容量:切片的长度就是它所包含的元素个数。切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5, 6}
printSlice(s)
// 截取切片使其长度为 0
s = s[:0]
printSlice(s)
// 扩展其长度
s = s[:4]
printSlice(s)
// 舍弃前两个值
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
Output:
len=6 cap=6 [1 2 3 4 5 6]
len=0 cap=6 []
len=4 cap=6 [1 2 3 4]
len=2 cap=4 [3 4]
Receivers in Golang
- 可以直接在函数里引用 receiver;
- Receiver 可以直接 call 这个 method。
//...
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := &Vertex{3, 4}
fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
func
In Golang
// 最简单形式
func FunctionName() {
// code to be executed
}
// 带参数的函数
func FunctionName(param1 type, param2 type, param3 type) {
// code to be executed
}
// 带返回值类型的函数
func FunctionName(param1 type, param2 type) type {
// code to be executed
return output
}
func myFunction(x int, y int) int {
return x + y
}
// 可以在这里指明返回变量名
func myFunction(x int, y int) (result int) {
result = x + y
return
}
// r 表示接收器。
func (r *ReconcileTrial) UpdateTrialStatusByClientJob() error {
if ... {
return err
}
return nil
}
type
In Golang / Type Embedding
下面这种怎么理解?
type ReconcileTrial struct {
client.Client
Log logr.Logger
//...
}
In fact, sometimes, a struct field can be composed of a field type only. The way to declare struct fields is called type embedding. Embedded fields are also called as anonymous fields. However, each embedded field has a name specified implicitly. The unqualified type name of an embedded field acts as the name of the field.
所以看下来其实是可以有名字的,比如说就是 Client。
interface
In Golang
必须要和 type
关键词搭配使用。
In Go, to implement an interface, a type must define all methods declared by the interface. 而且从下面我们也可以看出来,Implementation is implicit, meaning no keyword (such as implements) is needed.
// Define the interface
type Shape interface {
Area() float64
}
// Circle type that implements the Shape interface
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
把一个 interface 放到 struct 里面(type embedding^)属于显式继承吗?不是,参考这个回答:inheritance - Go - how to explicitly state that a structure is implementing an interface? - Stack Overflow
下面这种方式才是正确的继承方式(也就是隐式继承):
type Foo interface{
Foo()
}
type Bar struct {
}
func (b *Bar)Foo() {
}
var _ Foo = (*Bar)(nil)
struct
In Golang
后面的字符串是结构体标签。用于为 field 添加元数据,通常用于序列化和反序列化。标签的语法是一种自定义键值对的字符串格式。
type Bench struct {
metav1.TypeMeta `json:",inline"`
Spec `json:"spec,omitempty"`
}
for
In Golang
Go 只有 for
循环,没有 while
关键字,Go 的 while 循环是使用 for
关键字来实现的。
for condition {
// code block
}
// 死循环
for {
// 通过 break 来退出循环
break;
}
select
In Golang
和 switch
很像。在多个 channel^ 里选执行最快的那个 channel。
package main
import (
"fmt"
"time"
)
func portal1(channel1 chan string) {
time.Sleep(3 * time.Second)
channel1 <- "Welcome to channel 1"
}
func portal2(channel2 chan string) {
time.Sleep(9 * time.Second)
channel2 <- "Welcome to channel 2"
}
func main() {
R1 := make(chan string)
R2 := make(chan string)
// calling function 1 and function 2 in goroutine
go portal1(R1)
go portal2(R2)
select {
// case 1 for portal 1
case op1 := <-R1:
fmt.Println(op1)
// case 2 for portal 2
case op2 := <-R2:
fmt.Println(op2)
}
}
// Output
// Welcome to channel 1
如果多个 channel 同时 ready 了,那么随机选一个执行。这意味着上面这个例子如果我们改 channel2 的 sleep time 也为 3,那么有可能输出第一个也有可能输出第二个信息。
如果多个 channel 都没有信息,那么会发生 block,为了避免 block 其实可以使用 default 函数:
//...
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
default:
fmt.Println("No tasks are ready yet")
}
}
// output:
// No tasks are ready yet
Select Statement in Go Language - GeeksforGeeks
Golang 并发模型 / CSP (Communicating Sequential Processes)
Go 实现了两种并发形式:
- 第一种是大家普遍认知的:多线程共享内存。其实就是 Java 或者 C++ 等语言中的多线程开发;
- 另外一种是 Go 语言特有的,也是 Go 语言推荐的:CSP(communicating sequential processes)并发模型。
CSP 并发模型是在 1970 年左右提出的概念,属于比较新的概念,不同于传统的多线程通过共享内存来通信,CSP 讲究的是“以通信的方式来共享内存”。 名人名言:“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”
普通的线程并发模型,就是像 Java、C++、或者 Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。例如 Java 提供的包 java.util.concurrent
中的数据结构。Go 中也实现了传统的线程并发模型。
Goroutine / Go Channel
Go Channel,是 goroutines 之间通信的媒介。
Go 的 CSP 并发模型,是通过 goroutine 和 channel 来实现的。
- goroutine 是 Go 语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。
- channel 是 Go 语言中各个并发结构体 (goroutine) 之前的通信机制。 通俗的讲,就是各个 goroutine 之间通信的”管道“,有点类似于 Linux 中的管道。
Go 语言最大的特色就是从语言层面支持并发(其他的像 Python 是不是都只算是框架层面支持?)。事实上每一个 Go 程序至少有一个 Goroutine:主 Goroutine。当程序启动时,它会自动创建。
Go 语言中的协程。使用 go 关键字 go func()
即可启动一个协程,并且它是处于异步方式运行,你不需要等它运行完成以后再执行以后的代码。这点和 Python 的协程不一样,如上所述,Python 在主程序执行了 send()
函数后,需要等待子程序返回才能继续进行。
通信机制 channel 也很方便,传数据用 channel <- data
,取数据用 <-channel
。这就是为什么回到刚才说的:Go 语言最大的特色就是从语言层面支持并发。在通信过程中,传数据 channel <- data 和取数据<-channel 必然会成对出现,因为这边传,那边取,两个 goroutine 之间才会实现通信。而且不管传还是取,必阻塞,直到另外的 goroutine 传或者取为止。
package main
import "fmt"
// main() 本身也是运行了一个 goroutine
func main() {
// 声明一个阻塞式的无缓冲的通道
messages := make(chan string)
// 在这个刚刚启动的 goroutine 里,我们把 "ping" 这个信息传给了 channel messages
go func() { messages <- "ping" }()
// main goroutine 从 messages 里读了出来。
msg := <-messages
fmt.Println(msg)
}
Go goroutine理解 - 知乎:这篇文章后面还有对于 goroutine 机制的详细讲解,值得一读。
Golang 特有概念
Golang 代码规范
变量统一使用驼峰命名。
Golang 目录组织
/pkg
目录是 Go 语言项目中非常常见的目录,我们几乎能够在所有知名的开源项目(非框架)中找到它的身影,例如 Kubernetes、Prometheus、Moby、Knative 等。
该目录中存放可以被外部应用使用的代码库,其他项目可以直接通过 import 导入这里的代码。所以,我们在将代码库放入该目录时一定要慎重。
/cmd
: 一个项目有很多组件,可以把组件 main
函数所在的文件夹统一放在 /cmd
目录下。
Golang 标准库
init()
Function in Golang
go 语言中 init()
函数用于包 (package) 的初始化,该函数是 go 语言的一个重要特性。
- 每个包可以拥有多个 init 函数;
- 包的每个源文件也可以拥有多个 init 函数;
- 同一个包中多个 init 函数的执行顺序 go 语言没有明确的定义;
- 不同包的 init 函数按照包导入的依赖关系决定该初始化函数的执行顺序;
- init 函数不能被其他函数调用,而是在 main 函数执行之前,自动被调用;
Golang testing 库
testing 是 Go 语言标准库自带的测试库。
在 Go 中编写测试很简单,只需要在待测试功能所在文件的同级目录中创建一个以 _test.go
结尾的文件。在该文件中,我们可以编写一个个测试函数。测试函数名必须是 TestXxxx 这个形式,而且 Xxxx 必须以大写字母开头,另外函数带有一个 *testing.T
类型的参数:
测试编写完成之后,使用 go test
命令运行测试。
Golang time 库
time.NewTicker()
ticker := time.NewTicker(3 * time.Second)
函数的作用是返回一个 Ticker 类型的值,该值会在每隔指定的时间(参数 d)后向其自身的 Channel C 中发送一个时间值,表示指定的一个时间间隔已经过去了。使用 Ticker 可以在指定的时间间隔内周期性地执行某些任务。
func main() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("This is a demo for Ticker!")
}
}
}
// Output: 每隔一秒输出一句 This is a demo for Ticker!