Go 是一门 Google 开发的,已经在各领域,尤其是对性能要求较高的中间件领域得到广泛使用的后端编程语言。这门语言我在早期编写 Go 服务的 Dockerfile 时短暂接触过,但是对其语法望而生畏。我最擅长的语言是以 Java 为代表的 JVM 编程语言,和以 C、C++ 为代表的经典编程语言。在编写这些编程语言的程序时,常常感到语言设计上的不足,如 Java 的泛型擦除和几乎完全不支持运算符重载。近些年了解了 Rust、Go 这类新秀后,新型编程语言极大地吸引了我,但是忙于考研和找实习,一直没有时间学习这些语言。
偶然从群友处得知字节新一轮青训营时,鉴于错失今年年初青训营报名机会的惨痛教训,我马上提交了报名申请,希望借此机会学习和接触 Go。
我深知深入了解一门编程语言离不开其底层实现等细节的修炼,但还是希望在这短短的一个月青训营中学习到最贴近实际生产实践中 Go 的技术。为此,我同时借阅了《Go 程序设计语言》等书目,以期将青训营所学进一步巩固深化。
愿我们在更高处重逢!
语言特点
- 高性能、高并发:标准库即支持高并发
- 语法简单
- 标准库丰富:标准库成熟、稳定
- 工具链完善:自带代码检查、单元测试框架、性能测试框架、包管理工具等
- 静态连接:便于部署,特别是容器化时,镜像体积可以非常小
- 编译迅速:工程环境下编译也能做到一分钟以下
- 跨平台
- 运行时 GC
快速上手
// 代表本程序属于 main 包,即入口文件
package main
// 导入 fmt 包,此包用于格式化输出
import "fmt"
func main() {
fmt.Println("Hello Go!")
}
上述程序存储在 ./hello-go.go 中,通过 go run hello-go.go 指令执行如下:
Hello Go!
通过 go build hello-go.go 则可编译为二进制文件 hello-go。exe,执行 ./hello-go.exe 输出同样的结果。
基本语法
:= 和 =
参照《Go语言中 := 和 = 区别是什么?》,:= 和 = 的区别在于:
:= 用于变量声明和初始化,只能在函数内部使用。类似于 var name = val 的语法糖,会推导变量类型。
= 用于赋值,此操作不会推导变量类型。
变量类型
Go 是强类型语言,类型主要有字符串 string、整数 int、浮点数 float64 和 float32。
值得一提的是,字符字面量是 int32 类型。
可以通过下面的方式定义变量和常量。
var v0 = "v0"
var v1 string = "v1"
var v2, v3 string = "v2", "v3"
e0 := "e0"
e1, e2 := "e1", "e2"
const c0 = "c0"
const c1, c2 = "c1", "c2"
控制流语句
条件
if 和 else 必须带大括号,条件表达式不需要小括号。
循环
Go 中只有 for 循环。
for {
}
for i := 0; i < 10; i++ {
fmt.Println(i)
}
n := 0
for n < 3 {
n++
}
中途可以使用 continue 或 break。
分支
switch 和 if 类似,后不接小括号。相比 C、C++ 和 Java,Go 的分支支持任意变量类型,且每一个 case 结束后自动 break,除非用 fallthrough。
TODO: 支持原理
此外 switch 后面也可以不写表达式,而是在 case 里写表达式,以代替 if-else。
val := 10
switch {
case val < 0:
fmt.Println("Val < 0")
case val < 10:
fmt.Println("Val < 10")
case val < 20:
fmt.Println("Val < 20")
case val < 30:
fmt.Println("Val < 30")
}
类似于 C++ 的:
const std::int32_t val = 10;
do {
if (val < 0) {
std::cout << "Val < 0" << std::endl;
break;
}
if (val < 10) {
std::cout << "Val < 10" << std::endl;
break;
}
if (val < 20) {
std::cout << "Val < 20" << std::endl;
break;
}
if (val < 30) {
std::cout << "Val < 30" << std::endl;
break;
}
} while (false);
数组
数组是带编号且长度固定的元素序列,定义方式如下:
var a [5]int
默认值都是 0。通过 len 可获取长度,通过 [] 访问数据。
var a [5]int
a[0] = 5
fmt.Println(a)
fmt.Println(len(a))
输出如下:
[5 0 0 0 0]
5
也可以通过下面的方式定义数组:
a := [6]int{1, 2}
fmt.Println(a)
输出如下:
[1 2 0 0 0 0]
可见没有指定初始值的,默认初始化为 0。
二维数组是 [exp][exp]int。
Go 不支持 VLA:
数组遍历时,获得的是 (index, value),可以用 _ 忽略索引,如果不需要索引的话:
for i, v := range arr {
fmt.Println("arr[", i, "] = ", v)
}
切片
切片通过 make 构造,可以任意时刻更改数组的长度。
strs := make([]string, 2)
strs[0] = "Hello"
strs[1] = "Go"
fmt.Println(strs)
fmt.Println(len(strs))
输出如下:
[Hello Go]
2
通过 append 向切片尾部追加字符串 "!":
strs = append(strs, "!")
fmt.Println(strs)
fmt.Println(len(strs))
输出如下:
[Hello Go !]
3
值得注意的是,append 有可能返回一个新的切片。当原切片的 capacity 不足时,会构造一个新的切片。
复制切片:
c := make([]string, 1)
copy(c, strs)
fmt.Println(c)
输出如下:
[Hello]
顺便发现复制目标长度不够时,会截尾。
切片也可以再切片,和 Python 类似,使用 slice[beg?=0 : end?=len] 实现。
映射
make 也可以创建哈希表,例如:
m := make(map[string]int)
map[string]int 等价于 Map<String, Integer>。
查询值的时候,可以获取 contains 的结果,也可以忽略:
m := make(map[string]string)
m["Chuanwise"] = "Chuanwise"
fmt.Println(m["Chuanwise"])
val, ok := m["Chuanwise"]
if ok {
fmt.Println("OK, val: ", val)
} else {
fmt.Println("No such key-value pair")
}
val = m["Chuanwise"]
fmt.Println(val)
val = m["Lclbm"]
fmt.Println(val)
fmt.Println(len(val))
输出如下:
Chuanwise
OK, val: Chuanwise
Chuanwise
0
这也说明,出错时,返回的不是 null,而是 ""。无法分辨值也是空串的场景,因此应该检查获取结果。
删除值时用 delete(m map[K]V, key K) 实现。
map 遍历的顺序是完全随机的,既不会按照插入顺序,也不会按照自然顺序:
for k, v := range m {
fmt.Println(k, v)
}
如果不想要值,也可以用 for k := range m 或者 for k, _ := range m。
指针
指针类型是 *T,获取地址的方式是 &v。
函数
普通函数
func add(a int, b int) int {
return a + b
}
实际业务中,几乎所有函数都返回两个值,分别是结果和错误信息。
func exists(m map[string]string, key string) (string, bool) {
v, ok := m[key]
return v, ok
}
结构体及其方法
type person struct {
name string
age int
}
func (p person) getAgeWithCopying() int {
return p.age
}
func (p *person) getAgeWithoutCopying() int {
return p.age
}
构造时,需要指定值。
p := person{name: "Chuanwise"}
没有被指定值的字段会被初始化为空值。
错误处理
方式是用多返回值,而非异常或者全局 errno。
func returnsErr() (r int, err error) {
return 0, errors.New("Error!")
}
字符串操作
字符串操作都在 strings 包下,主要有 Contains、Count、HasPrefix、HasSuffix、Index、Join、Repeat、Split、ToLower 和 ToUpper。其中 Index 类似于 java.lang.String#indexOf(CharSequence)。
字符串格式化
%v 可以打印 任意类型的变量,但对结构体而言只会打印值,类似于 {v1 v2}。除了使用这种方式,还可以:
%+v:字段名和值,类似于{f1:v1 f2:v2}%#v:包含结构体名的更为完整的字符串,类似于main.point{f1:v1 f2:v2}。
此外用 %.2f 之类的方式可以打印浮点数。
JSON
JSON 借助包 encoding/json。结构体里,大驼峰的字段会被认为公开字段,json.Marshal() 只会序列化公开字段。序列化得到 []byte,应该用 string(res) 将其转化为字符串并输出。
type person struct {
Name string
Age int
}
func main() {
p := person{}
s, _ := json.Marshal(p)
fmt.Println(string(s))
}
输出如下:
{"Name":"","Age":0}
反序列化为 json.Unmarshal(buf, &obj)。
类型字段可以加 tag,类似于:
type person struct {
Name string
Age int `json:"age"`
}
类似于 @JsonAlias 的注解。
时间处理
time.Now() 获取当前时间,time.Date(...) 可以构造指定时间。
格式化时,可以给出把 2006-01-02 15:04:05 在你的时间格式下的格式字符串来格式化。例如:
now := time.Now()
fmt.Println(now.Format("15:04:05 01-02-2006"))
输出如下:
23:12:28 05-12-2023
不像其他语言要 YYYY-MM-dd 之类的。
字符串和数字转换
strconv 处理字符串和数字之间的转换。
f, _ := strconv.ParseFloat("1.234", 64)
n, _ := strconv.ParseInt("111", 0, 64)
其中,ParseInt 的第二个参数 0 表示进制,0 是让程序去推测;64 是返回 64 位整数。
此外,也有 Atoi。
进程信息
os.Args 可以获取进程的命令行参数,os.Getenv("PATH") 可以获取环境变量,os.Setenv("name", "value) 设置环境变量。
此外,exec.Command(...) 可以启动子进程。类似于 Runtime#exec。