Golang Quick Start

Introduction

Go users: https://github.com/golang/go/wiki/GoUsers web services, devops: docker + k8s

Visual Studio Code go-plugin, vim go-plugin

Install and set up a runnable demo project, a brief guidance can see here: Official Setup Go project

If forget, read through the tutorials one by one: https://go.dev/doc/

Other resources:

What are go package and module is explained here: https://go.dev/doc/code A package is a collection of source files in the same directory that are compiled together. Functions, types, variables, and constants defined in one source file are visible to all other source files within the same package.

A repository contains one or more modules. A module is a collection of related Go packages that are released together. A Go repository typically contains only one module, located at the root of the repository. A file named go.mod there declares the module path: the import path prefix for all packages within the module. The module contains the packages in the directory containing its go.mod file as well as subdirectories of that directory, up to the next subdirectory containing another go.mod file (if any).

An import path is a string used to import a package. A package’s import path is its module path joined with its subdirectory within the module. For example, the module github.com/google/go-cmp contains a package in the directory cmp/. That package’s import path is github.com/google/go-cmp/cmp. Packages in the standard library do not have a module path prefix.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// A module is a collection of related Go packages that are released together
// in the first line of go.mod, you can see the module name

// main package and func is the entrypoint of app
// the file name can be others not necessary as main.go
package main

import (
. "fmt" // call fmt funcs directly withou fmt prefix
err "errors" // alias
"os"
"net/http"
"regexp"
"builtin" // no need to import
_ "github.com/ziutek/mysql" // call init in the package
)

// auto append `;`, so let { at the same line
func main() {
fmt.Println("Hello world")
}

package name in go file should be the same as folder name.

Important env variables:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
## https://maelvls.dev/go111module-everywhere/
## In 1.15, it's equivalent to auto.
## In >= 1.16, it's equivalent to on
GO111MODULE=""

## you can export or use `go env -w`
## GOROOT is set automatically
export GOROOT=/usr/local/go
export GOBIN=$GOROOT/bin

## only need to set GOPATH
## go path is the project workspace
## it has src (user create), pkg and bin (auto create for you when build or install)
export GOPATH=$HOME/go
export PATH=$PATH:$GOBIN

## persistent set and unset
go env -w GOPATH=$HOME/go
go env GOPATH
go env -u GOPATH

Understand run, build, install, get subcommands. pluralsight has Go CLI playbook course.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
go version

## used GO >= 1.16
## has version suffix
## install binary without changing go.mod
go install sigs.k8s.io/kind@v0.9.0
go install sigs.k8s.io/kind@latest

## create a module
## link with your code repo url
## generate go.mod
go mod init example.com/example
## add missing and remove unused modules
## can edit the package version in require block
go mod tidy

# clean mod cache
go clean --modcache

## link dependency to local path
## ../greetings is a local package in relative path
## example.com/greetings is that packages module path
## see this tutorial:
## https://go.dev/doc/tutorial/call-module-code
go mod edit -replace example.com/greetings=../greetings
go mod tidy

# download dependencies for off-line go build
# see this ticket:
# https://stackoverflow.com/questions/68544611/what-is-the-purpose-of-go-mod-vendor-command
go mod vendor

## GO >=1.16, only for editing go.mod
## -x: verbose
## it will modify the go.mod
## or you edit go.mod manually
go get github.com/sirupsen/logrus@v1.8.0

## simliar to python dir()/help()
## case-insensitive
go doc fmt
go doc net/http
go doc time.Since
go doc fmt.Println
## local API server , browser your own package
godoc -http :8080

## linter, but VSC will auto does that when save go files
go fmt <package># it runs gofmt -l -w

## test
## file with _test.go suffix
go test [-v] [-run="Hello|Bye"] [-cover|-coverprofile]

## -n: dry run
go run -n main.go
## -work: print $WORK temp folder
## you will see the intermediary files created
go run -work main.go
## run main package
go run main.go
go run .
go run

## build(compile) not install
go build hello
## it will create executable in bin folder
cd $GOPATH/bin
## executable name is the same as folder name
./hello

## install dir is control by
## env GOPATH and GOBIN
go install hello

# check doc from CLI
go doc rand.Float64
go doc math.Sin
go doc strconv.Atoi

数据类型

基本数据类型: int32, float64, bool, string(没有char type) 复合数据类型: array, slice, map, function, pointer, struct, interface

&(取地址符,没有地址的算数运算), *t

变量赋值方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var xx = 23
var xx int = 23
var xx float32 = 2.32

// var block
var (
xx int = 34
yy = 78.99
zz = "123"
)
// 简短写法
a := "hello"

// swap
b := "world"
a, b = b, a

// 全局变量不支持简短写法
var GLOBAL int = 100

常量使用

Use all upper cases

字符串类型

1
2
str1 := "abc"
str2 := `abc`

强制类型转换

1
int()

算数运算符

关系运算符

<, >, <=, >=, ==, !=

逻辑运算符

&&, ||, !

位运算符

&, | , ^, &^(位清空), <<, >>

If-else

condition 没有括号, no ternary

1
2
3
4
5
6
// if 还可以有初值,和for loop类似了
// 也可以把初值写到外面
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}

Switch

每个case 自带break。 关键字 fallthrough 只能放case最后一行, 连接执行2个case且一定执行。 case 后的数据类型必须和switch一致, case 可无序,多个condition可用逗号分开。 switch 省略条件相当于switch true(tagless switch), 也可省略后,把condition 写在case 后面,就像if-else if了 也可以有初值switch t = 100 {..}, t only use in switch block

For loop

no while loop 语法和C一样: for init; conditionl; post {} condition no bracket

1
2
3
4
for {}
for ;; {}
for key, value := range map {}
for index, value := range [slice, array, string] {}

Goto

前后跳都可以

Array

没赋值的默认值和C语言一样:

1
2
3
4
5
6
7
8
9
10
11
var a [10]int // element default is 0
var a = [10]int{}
var a = [3]float64 {1, 2, 3}
a := [3]float64 {1, 2, 3}
// 可以设置index对应的值
a := [3]float64 {0:32, 2:99}
a := [...]float64 {0:32, 2:99}
len(a)
cap(a)
// ascending sort
sort.Ints(a)

对于数组 len(a) 长度= cap(a) 容量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// same address usage as C language
var arr = [3]int32{99, 100, 101}

// 数组是值传递, 改变arr2中的值不会改变arr, 函数中注意, 除非传递的是地址
// 可直接复制
arr2 := arr
// true
fmt.Println(arr2 == arr)
// type [3]int
fmt.Printf("%T\n", arr2)
// 注意在go中数组名不是地址了,要专门用取地址符号
// 这2个值一样的
fmt.Printf("%p\n", &arr)
fmt.Printf("%p\n", &arr[0])

// t1 其实是指针类型 *[3]int32
t1 := &arr
fmt.Printf("%d\n", (*t1)[0])

二维数组

1
2
3
var arr := [3][3]int{{},{},{}}
len(arr)
len(arr[0])

Slice

动态数组,大小可变,背后有一个底层数组.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 不写长度, slice = nil, no underlying array
// have to append to use it
var slice1 []int
var slice1 = []float64 {1, 2, 3}
// or
// make(type, len, capacity) len 必须小于或等于 cap
// make function is in builtin package
// used to create object
slice1 := make([]int, 2) // len, cap = 2
slice1 := make([]int, 0, 2) // len = 0, cap = 2
// slice1中存的就是切片的地址,不需要取地址符了
// 而数组的地址则是&arr
fmt.Printf("%p\n", slice1)

slice1 = append(slice1, 1, 2, 3, 4, 5, 6, 7)
slice2 := []int{10, 11}
// 注意这里用...,类似于python的分解操作,如果查看append的定义,其实它的末尾参数是个可变参数...type
// 当append超过容量后,生成新的切片的,地址就变了
// 自动扩容是之前的2倍
slice1 = append(slice1, slice2...)

// map, slice 是地址传递!
// 指向同一个地址
slice3 := slice1

// create slice from array
// slice1和arr1指向同一个地址,共享数据,除非slice1扩容则会新生成一个内存地址
arr1 := [10]int
// [start, end)
slice1 := arr1[:]
slice1 := arr1[0:10]
slice1 := arr1[2:4]
// 这时候slice1地址和arr1不一样了
slice1 = append(slice1, 4, 5, 6)

// deep copy, for []type
copy(dst, src)
copy(slice2[2:], slice[1:])

Map

To understand map, see this question and the comment: https://stackoverflow.com/questions/40680981/are-maps-passed-by-value-or-by-reference-in-go this is a go playground for map to help understand its behavior:

If a map isn’t a reference variable, what is it?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// nil map, 不能直接用
var map1 map[int]float64
fmt.Println(map1 == nil)

map1 := make(map[int]string)
map1 := map[string]int{}
map1 := map[string]int{"hello": 100, "Java": 99}

map1["see"] = -34
val1 := map1["hello"]

// 如果map没有key,取出来的是value类型的"0"值
// 怎么判断呢? ok is bool, if true, then exist
val, ok := map["key"]

delete(map1["see"])
len(map1)

String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// UTF-8 coding
str := "hello"
str := `hello`
// string is acutally byte sequence
slice := []byte{65, 66, 67, 68}
str1 := string(slice1)
slice := []byte(str1)

substr := str[1:3]
// strings package 主要是字符串 操作函数
// strconv package 主要是字符串 和 基本数据类型之间相互转换
// go '+' operands must have same type, so use Sprintf to concat
// different type
"123" + strconv.Itoa(100)
b1, err := strconv.ParseBool("true")
str1 := strconv.FormatBool(b1)

defer

用于延迟函数或方法的执行, 用defer 控制的调用会等到它的containing/surrounding function执行完了之后再执行,并且当所有延迟的部分执行结束之后,containing function才执行return 语句。

如果有多个defer,则按照后进先出的顺序。 用法:

  • 关闭连接,文件,比如defer close() 在函数的开头.
  • panic, recover,defer函数执行完毕后,异常才会被抛出到上一层.

注意,延迟函数的arguments的值,在defer的时候就固定了,值也可能是地址,则复合对象的内部值可能被改变了。

1
2
3
4
5
func deferTest() {
defer fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
}

Function

函数也是一种复合数据类型,可以用%T 查看。 注意函数的参数类型和参数名是反过来写的,且返回值类型列表也写在最后。 函数名如果大写开头 表示公共函数,可被其他package调用,比如fmt.Println(),否则只能package内部访问。 参数传递没有python这么多形式。

参数传递也有值传递和引用(地址)传递. 值传递: int, float, string, bool, array, struct 引用传递: slice, map, chan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// (int, int) 对应了 return val1, val2的类型
// 注意这里p2是slice, p3是array
func funcName(p1 int, p2 []float, p3 [10]int, p4 map) (int, int) {
// logic
return val1, val2
}
// 或者只写return,返回列表里已经有了返回值名字了
func funcName(p1 int, p2 []float) (val1 int, val2 int) {
val1 = p1 * 3
val2 = len(p2)
// return named values
return
}
// 或者return 单纯用来结束函数
// 注意这里没有定义返回值列表
func funcName() {
return
}

// 如果只有一个返回值,可以不写括号
// 多个参数类型一致,可以把类型写在后边一起
func funcName(p1, p2 int, p3 string) int {
return val1
}

可变参数...,只能放到参数列表的最后,且只能有一个可变参数:

1
2
3
4
5
6
7
8
9
// ...type,0个到多个参数都可以,比如Println()就是
// nums 是个slice类型
func funcName(p1 int, nums ...int) {
// pass
}
// 调用的时候注意,如果是复合类型数据需要分解
funcName(100, 1, 2, 3)
// split sign `...`
funcName(100, []int {1, 2, 3}...)

可以定义函数变量,然后赋值,其实赋值的是函数的地址:

1
2
var c func(a ...interface{}) (n int, err error)
c = fmt.Println

Anonymous functions and closures 匿名函数,没有函数名,可以赋值给变量 或 直接调用. 所以说Go是支持函数式编程的,匿名函数可以作为其他函数的参数(这个作为参数的函数就叫回调函数),注意不是函数执行的返回值作为参数,是函数本身! 匿名函数也可以作为返回值(闭包)

1
2
3
4
5
6
7
8
9
// no function name and call it
func () {
fmt.Println()
}()
// or
fun3 := func () {
fmt.Println()
}
fun3()

回调函数:

1
2
3
4
5
6
7
8
9
10
11
// callback function
func add(a, b int) int {
return a + b
}
// calling function
// here we can define func type to replace 'func(int, int)int' in argument list
func operate(a, b int, fun func(int, int)int) int {
return fun(a, b)
}
// call
operate(1, 3, add)

闭包closure, 这里外部函数返回后,匿名函数把外部函数内部的资源i保留了下来。Python中local function一样的道理. A closure is a function value that references variables from outside its body.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// `func() int` is treated as return type as a whole
// the increment() return a closure
func increment() func() int {
// stay alive in closure function
i := 0
return func () int {
i++
return i
}
}

res := increment()
// 1
fmt.Println(res())
// 2
fmt.Println(res())
// 3
fmt.Println(res())

Pointer

指针类型符号*, 和C语言一样.

1
2
3
4
5
6
7
8
9
10
11
// nil is pointer's default vaule
var p1 *int
a := 10
p1 = &a
// print pointer value
fmt.Println(p1)
fmt.Printf("%p\n", p1)

// 指针的指针
var pp1 **int
pp1 = &p1

数组指针,指向数组的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arr := [3]int {1, 2, 3}
var pa *[3]int
pa = &arr
// 这个输出注意,并不是输出的地址数值,而是一种形式
// 如果要输出地址数值,用%p
fmt.Println(pa) // &[1 2 3]

// the address of arr
fmt.Printf("%p\n", pa)
fmt.Printf("%p\n", &arr)

// 相同的表达
(*pa)[0]
pa[0] // (*pa)[0] 的简化写法
arr[0]

指针数组,元素是指针的数组

1
2
3
4
5
a := 1
b := 2
c := 3
arr := [3]*int {&a, &b, &c}
fmt.Println(arr)

For example:

1
2
3
4
5
6
// 指向 指针数组的指针
*[3]*int
// 指向 数组指针的指针
**[5]string
// 指向 指针数组指针的指针
**[3]*string

函数指针,指向函数的指针, Go中函数名就是函数的地址,如同slice, map一样:

1
2
var c func(a ...interface{}) (n int, err error)
c = fmt.Println

指针函数,返回指针值的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 2个地址输出是一样的
func getArr() *int[4] {
arr := [3]int {1, 2, 3}
fmt.Printf("%p\n", &arr)
return &arr
}

arr := getArr()
fmt.Printf("%p\n", &arr)
```

# Struct
Usually struct has [struct tag](https://stackoverflow.com/questions/55544525/what-is-the-string-after-the-name-and-type-in-a-struct-in-golang).
```go
type Person struct {
name string
age int
sex string
address string
}

// 几种创建方式
// p1 is an empty struct object with default values
var p1 Person
// same as p1
// 可以拿到外面赋值, 比如p2.age = 34
p2 := Person{}

p3 := Person (name: "XXX", age: 23, sex: "female", address: "YYY")
p4 := Person {
name: "XXX",
age: 23,
sex: "female",
address: "YYY"
}
// order matters
p3 := Person ("XXX", 23, "female", "YYY")

结构体是值类型的,和array一样,结构体名不是它的地址。

1
2
3
4
5
6
7
8
9
// 这是内容复制
p4 := p1

// 这是地址赋值
var pp1 *Person
pp1 = &p1
// 访问字段,以下都可以
pp1.name
(*pp1).name

make只能创建map, slice, channel等 make vs new: https://www.godesignpatterns.com/2014/04/new-vs-make.html new returns pointer:

1
2
3
4
5
6
// they are the same
var p1 Person
pp1 := &p1

pp1 := &Persion{}
pp1 := new(Persion)

new可以创建任意类型空间 和 返回任意类型的指针:

1
pint := new(int)

匿名结构体,和匿名函数类似的定义用法

1
2
3
4
5
6
7
8
// 这里定义了一个匿名结构体 同时进行赋值
s2 := struct {
name string
age int
}{
name: "xxx",
age: 18,
}

匿名字段,字段没有名字,且字段类型不能重复:

1
2
3
4
5
6
7
8
9
10
11
type Worker struct {
// 只能有一个string
string
// 只能有一个int
int
}

w1 := Worker {"xxx", 11}
// 默认使用数据类型作为访问的名字
fmt.Println(w1.string)
fmt.Println(w1.int)

结构体嵌套:

1
2
3
4
5
6
7
8
9
10
11
12
13
type A struct {
a string
}

type B struct {
b string
c A
}

s := B {b: "from B", c: A {a: "from A"}}
// access field
s.b
s.c.a

如果对嵌套结构体使用了匿名字段,则相当于融合到了当前结构体,访问的时候不用中间层了。这在OOP 中使用到了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type A struct {
a string
}

type B struct {
b string
A
}

// 注意这里匿名字段初始化A: A{a: "from A"}
s := B{b: "from B", A: A{a: "from A"}}
// access field
s.b
// 中间层被省略了
s.a

Type

type 除了定义结构体,接口,还可以定义全新的类型 或者 别名: function type and value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// myint, mystr 就是全新的类型了 
type myint int
type mystr string

// 不能和原类型相互赋值!
var myint = 23
var mystr = "hello"

// myint, mystr 只是一个别名,注意和上面的区别
type myint = int
type mystr = string

// 可以和原类型相互赋值!
var a int = 10
var b myint = a

Go语言实现函数式编程的时候,如果函数复杂,可以用type 简化,这里的例子是作为返回值,也可以作为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 把func(string, string) string 整体别名叫做myfun
type myfun func(string, string) string

// myfun 作为fun1 的返回值类型,就不用写一大堆了
func fun1() myfun {
// 返回一个函数
return func(a, b string) string {
return a + b
}
}

// 调用, res得到一个函数定义
res := fun1()
// "100100"
fmt.Println(res("100", "100"))

还需要注意的是,一个package中定义的别名,不能在其他包中为它添加method.

Error

Go中不要把错误 和 异常弄混了,这个还要单独看其他文章区分一下使用环境。

Go中错误也是一种数据类型error, 惯例是error 在函数中返回值的最后一个位置, error其实是一个接口:

1
2
3
type error interface {
Error() string
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {
"os"
"log"
"fmt"
}

func main() {
// 很多包中的函数都会有错误信息返回,特别是文件,网络操作
f, err := os.Open("./test.txt")
if err != nil {
log.Fatal(err)
// 得到具体的错误类型,输出其他字段
// *os.PathError 是这个函数返回的错误类型
if ins, ok := err.(*os.PathError); ok {
// 错误中的字段
fmt.Println(ins.Op)
fmt.Println(ins.Path)
fmt.Println(ins.Err)
// 如果错误实现了自己的方法,调用方法也行
}
}
// pass
}

自己如何创建错误呢, 通过errors.New这个package提供的函数 或者fmt.Errorf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {
"errors"
"fmt"
}

func main() {
err1 := errors.New("my error")
fmt.Println(err1)
// 是 *errors.errorString 类型
fmt.Printf("%T\n", err1)

err2 := fmt.Errorf("my error is %d", 100)
fmt.Println(err2)
// 是 *errors.errorString 类型
fmt.Printf("%T\n", err2)
}

以上只是简单的例子,复杂的error 是通过结构体实现的,里面包含了错误的具体信息,然后实现了error 这个接口.

Panic and Recover

defer 一起使用较多,遇到panic,函数后续会停止执行,然后逆序执行所有已经遇到的当前层的defer函数,没遇到的不会执行,因为中断了,最后往上层抛出异常, defer 函数中可能包含recover 来恢复panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func A() {
fmt.Println("from A")
}

func B() {
fmt.Println("from B")
defer fmt.Println("from B defer 1")
for i := 0; i <= 10; i++ {
fmt.Println("i = ", i)
if i == 5 {
// panic throw
panic("panic happened")
}
}
// will not execute this defer
defer fmt.Println("from B defer 2")
}

func main() {
// 捕捉恢复了panic
defer func() {
if msg := recover(); msg != nil {
fmt.Println("from recover")
}
}

A()
defer fmt.Println("from main defer 1")
B()
// 这后面不会被执行了
defer fmt.Println("from main defer 2")
fmt.Println("from main end")
}

哪些场景适合使用panic呢?

  • 空指针
  • 下标越界
  • 除数为0
  • 分支没有出现
  • 错误输入
0%