Go语言基础
什么是Go语言
Go(又称 Golang )是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go 语言语法与 C 相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style 并发计算。
Go语言有如下特点:
- 高性能、高并发:有和 java、C++ 媲美的性能,只需要使用标准库,或者任意基于标准库的第三方库,即可开发高并发应用程序。
- 语法简单、学习曲线平缓:语法风格类似于 c 语言,并且在 c 语言的基础上进行了大幅度的简化,如去掉了不需要的表达式括号,循环也只有 for 循环一种表示方法。
- 丰富的标准库:和 python 一样,拥有极其丰富,功能完善,质量可靠的标准库,很多情况下,不需要借助第三方库就可以完成大部分基础功能的开发,大大降低了学习和使用成本。标准库有很高的稳定性和金融性保障,还能持续享受语言迭代带来的持续性优化,是第三方库所不具备的。
- 完善的工具链:Go 语言诞生之初,就拥有丰富的工具链,无论是编译,代码格式化,错误检查,帮助文档,包管理还是代码补充提示,都有对应的工具。Go 语言还内置了完整的单元测试框架,能够支持单元测试,性能测试,代码覆盖率,性能优化,这些都是保证代码能够正确和稳定运行的必备利器。
- 静态链接:在 Go 语言里,所有的编译默认都是静态链接,只需要拷贝编译之后的唯一一个可执行文件,不需要附加任何东西就能部署运行。
- 快速编译:Go 语言有静态语言里面几乎最高的编译速度。
- 跨平台:Go 语言本身能在常见的 Windows,Linux,MacOS 等操作系统下面运行,也能用来开发安卓,ios 软件,能够在各种设备上运行,包括路由器等。Go 语言还具备很方便的交叉编译特性,能够轻易地在笔记本上编译出一个二进制,拷贝到路由器上运行,还无需配置交叉编译环境。
- 垃圾回收:和 java 类似,写代码时,无需考虑内存的分配释放,可以专注于业务逻辑。
\newline
入门
开发环境
安装Golang
可以直接在浏览器输入 go.dev 打开 Golang 的官网,然后点击 download 然后按照提示就可以安装Golang。Golang的环境配置这里不详解,请参考其他博文。
如果打不开,可以尝试使用 Golang 中国的镜像 。
如果访问 github 的速度非常慢,可以配置 go mod proxy,打开 goproxy 按照提示操作即可,配置完成后下载第三方包的速度会大大加快。
\newline
配置集成开发环境
go 的开发环境可以选择 VSCode 或者 Goland,这是如今功能最多、使用最广泛的编辑器或者 IDE。
VSCODE 是一款由微软公司开发的,能运行在 Mac OS、Windows 和 Linux 上的跨平台开源代码编辑器。虽然它是一款编辑器,但是它可以通过扩展程序为编辑器实现,包括代码高亮、代码提示、编译调试、 文档生成等功能。配置完成之后可以视为一个功能齐全的IDE。
如果要安装 VSCode ,直接从官网下载安装即可,安装完成之后,需要在左边扩展里面搜索 Go 插件,然后安装。
Goland 是由 JetBrains 公司开发的一个新的商业 IDE, 相比 vscode,它在重构、代码生成等方面做得更好。Goland 是一个收费软件,可以直接从官网下载,可以 30 天免费试用,对于在校学生可以申请免费的教育许可证,在校期间都可以直接免费使用。
\newline
基于云的开发环境
如果有 github 的话,可以很方便地使用 gitpods.IO 的在线编程环境来试用 golang。只需要浏览器打开 项目代码样例 ,用 github 账号登录,就可以打开这个课程的示例项目开始编码。
\newline
基础语法
Hello World
以下是一个简单的main.go程序:
package main // 程序的入口包
import (
"fmt" // 导入标准库的FMT包,往屏幕输入输出字符串、格式化字符串
)
func main() {
fmt.Println("hello world")
}
package main;
代表这个文件属于 main 包的一部分,main 包也就是程序的入口包。
第三行导入了标准库里面的 FMT 包。这个包主要是用来往屏幕输入输出字符串、格式化字符串。
import 下面是 main 函数,main 函数里调用了 fmt.Println 输出 helloword。要运行这个程序,直接在对应目录下打开终端,输入 go run main.go:
如果使用的是vscode,配置好环境后,直接右键点击Run Code,可以在终端看到打印结果:
如果想编译成二进制,可以在 go buid 来编译,编译完成之后直接 ./heloword 就可以运行。
在 FMT 包里面还有很多的函数来做不同的输入输出格式化工作,可以在编辑器里面把鼠标悬浮在相应代码上,就可以看到每一个函数的文档。
也可以进入 pkg.go.dev ,后面加上相应的包名,比如 FMT ,然后就能看到这个包的在线文档,可以从里面去挑选需要的函数来使用。
\newline
变量
变量类型
go语言是一门强类型语言,每一个变量都有它自己的变量类型。常见的变量类型包括:字符串、整数、浮点型、布尔型等。go 语言的字符串是内置类型,可以直接通过加号拼接,也能够直接用等于号去比较两个字符串。在 go 语言里面,大部分运算符的使用和优先级都和 C 或者 C++ 类似,这里就不再概述。
\newline
变量声明
在 go 语言里面变量的声明有两种方式,一种是通过 var 变量名 = 值 这种方式来声明变量,声明变量的时候,一般会自动去推导变量的类型。如果有需要,也可以显示写出变量类型。另一种声明变量的方式是: 变量名 := 值。
\newline
常量
常量就是把 var 改成 const, go 语言里面的常量,没有确定的类型,会根据使用的上下文来自动确定类型。
通过以下代码体会变量和常量:
package main
import (
"fmt"
"math"
)
func main() {
// 第一种变量声明方式:var 变量名 = 值
var a = "initial" // 自动推导变量类型:字符串
var b, c int = 1, 2 // 显式写出变量类型:整数
var d = true // 布尔型
var e float64 // 浮点型
// 另一种变量声明方式:变量名 := 值
f := float32(e)
g := a + "foo"
fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
fmt.Println(g) // initialapple
const s string = "constant" // 常量
const h = 500000000
const i = 3e20 / h
fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}
\newline
if else
go 语言的 if else 写法和 C 或者 C++ 类似。
不同点:
- if 后面的条件判断没有括号()。即使写了括号,在保存时编辑器也会自动把它去掉。
- if 后面必须接大括号{},即不能像 C 或者 C++ 一样,直接把 if 里面的语句放在同一行。
通过以下代码体会条件语句的写法:
package main
import "fmt"
func main() {
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}
\newline
循环
在 go 里面没有 while 循环、do while 循环,只有 for 循环。最简单的 for 循环就是在 for 后面什么都不写,代表一个死循环。循环途中可以用 break 跳出,也可以使用经典的 C 循环。for 后面紧接着的三段语句,任何一段都可以省略。在循环里面,可以用 break 跳出循环,用 continue 继续循环。
通过以下代码体会循环语句的写法:
package main
import "fmt"
func main() {
i := 1
for {
fmt.Println("loop")
break
}
for j := 7; j < 9; j++ { // 经典的 C 循环
fmt.Println(j)
}
for n := 0; n < 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
for i <= 3 {
fmt.Println(i)
i = i + 1
}
}
\newline
switch
下面是关于 go 语言里面的 switch 分支结构,也与 C 或者 C++ 比较类似。同样地,switch 后面的变量名,不需要括号()。不同的是,在c++里面,switch case 如果不加 break 会继续执行下方所有的 case,在 go 语言里面不需要加 break。 go 语言的 switch 功能更强大,可以使用任意的变量类型,甚至可以用来取代相比 C 或者 C++ 任意的 if else 语句,可以在 switch 后面不加任何的变量,然后在 case 里面写条件分支,相比用多个 if else 语句,逻辑会更为清晰。
通过以下代码体会switch语句的写法:
package main
import (
"fmt"
"time"
)
func main() {
a := 2
switch a {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
case 4, 5:
fmt.Println("four or five")
default:
fmt.Println("other")
}
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}
\newline
数组
数组是一个具有编号且长度固定的元素序列。对于一个数组,可以很方便地存储/读取特定索引的值,也能直接打印一个数组。在真实业务代码里,很少直接使用数组,因为它长度是固定的,更多时候用的是切片。
通过以下代码体会数组的写法:
package main
import "fmt"
func main() {
var a [5]int // 存放5个int元素的数组
a[4] = 100
fmt.Println("get:", a[2])
fmt.Println("len:", len(a))
b := [5]int{1, 2, 3, 4, 5}
fmt.Println(b)
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}
\newline
切片
切片不同于数组,可以任意更改长度,然后也有更多丰富的操作。比如说可以用 make 来创建一个切片,可以像数组一样去取值,使用 append 来追加元素,注意必须把 append 的结果赋值为原数组。slice 的原理:存储了长度,容量,以及指向数组的指针,在执行 append 操作时,如果容量不够,会自动扩容,并且返回新的 sice,slice 初始化时也可以指定长度。slice 拥有像 python 一样的切片操作,但不支持负数索引。
通过以下代码体会切片的写法:
package main
import "fmt"
func main() {
s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get:", s[2]) // c
fmt.Println("len:", len(s)) // 3
s = append(s, "d")
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]
c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]
fmt.Println(s[2:5]) // [c d e]
fmt.Println(s[:5]) // [a b c d e]
fmt.Println(s[2:]) // [c d e f]
good := []string{"g", "o", "o", "d"}
fmt.Println(good) // [g o o d]
}
\newline
map
在其他编程语言里面,它可能可以叫做哈希或者字典,map 是实际使用过程中最频繁用到的数据结构。可以用 make 来创建一个空 map ,需要两个参数,key(string类型),value(int类型) 。可以从 map 里存取键值对,用 delete 删除键值对等。golang 的map是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。
通过以下代码体会map的写法:
package main
import "fmt"
func main() {
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
fmt.Println(m) // map[one:1 two:2]
fmt.Println(len(m)) // 2
fmt.Println(m["one"]) // 1
fmt.Println(m["unknow"]) // 0
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false
delete(m, "one")
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
}
\newline
range
对于 slice 或 map ,可以用 range 来快速遍历,简化代码。range 遍历的时候,对于数组会返回两个值,第一个是索引,第二个是索引对应的值。如果不需要索引,可以用下划线来忽略。
通过以下代码体会range的写法:
package main
import "fmt"
func main() {
nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
sum += num
if num == 2 {
fmt.Println("index:", i, "num:", num) // index: 0 num: 2
}
}
fmt.Println(sum) // 9
m := map[string]string{"a": "A", "b": "B"}
for k, v := range m {
fmt.Println(k, v) // b 8; a A
}
for k := range m {
fmt.Println("key", k) // key a; key b
}
}
\newline
函数
以下是 Golang 里面一个简单的实现两个变量相加的函数。 Golang 和其他很多语言不一样的是,变量类型是后置的。Golang 里面的函数原生支持返回多个值,在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个值是错误信息。
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func add2(a, b int) int {
return a + b
}
func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok
}
func main() {
res := add(1, 2)
fmt.Println(res) // 3
v, ok := exists(map[string]string{"a": "A"}, "a")
fmt.Println(v, ok) // A True
}
\newline
指针
go 里面也支持指针,但相比 C 和 C++ 里面的指针,支持的操作很有限。指针的一个主要用途就是对传入的参数进行修改。以下代码中的函数add2(),想把一个变量+2,但是这种写法是无效的,因为传入函数的参数实际上是一个拷贝,即仅对拷贝的变量进行了 +2, 并不起作用。正确的方法是需要将其写成指针类型add2ptr(),为了类型匹配,在调用此种函数时,参数需要加一个 & 符号。
package main
import "fmt"
func add2(n int) { // 仅对copy的变量进行操作,不起作用
n += 2
}
func add2ptr(n *int) {
*n += 2
}
func main() {
n := 5
add2(n)
fmt.Println(n) // 5
add2ptr(&n)
fmt.Println(n) // 7
}
\newline
结构体
结构体是带类型的字段的集合。以下代码样例中 user 结构包含了两个字段,name 和 password。可以用结构体的名称去初始化一个结构体变量,构造的时候需要传入每个字段的初始值,也可以用这种键值对的方式去指定初始值,这样可以只对一部分字段进行初始化。结构体也能支持指针,能够实现对结构体的修改,也可以在某些情况下避免一些大结构体的拷贝开销。
package main
import "fmt"
type user struct {
name string
password string
}
func main() {
a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}
c.password = "1024"
var d user
d.name = "wang"
d.password = "1024"
fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
fmt.Println(checkPassword(a, "haha")) // false
fmt.Println(checkPassword2(&a, "haha")) // false
}
func checkPassword(u user, password string) bool {
return u.password == password
}
func checkPassword2(u *user, password string) bool {
return u.password == password
}
\newline
结构体方法
在 Golang 里面可以为结构体去定义一些方法,类似于其他语言里面的类成员函数。以下代码样例中实现了上面一个例子的 checkPassword()函数,将其从一个普通函数,改成了结构体方法,这样用户可以使用 a.checkPassword(xx)
去调用。具体的代码修改,就是把第一个参数,加上括号,写到函数名称前面。
在实现结构体的方法的时候也有两种写法,一种是带指针,一种是不带指针,区别是如果带指针,可以对这个结构体进行修改;如果不带指针,实际上操作的是一个拷贝,就无法对结构体进行修改。
package main
import "fmt"
type user struct {
name string
password string
}
func (u user) checkPassword(password string) bool {
return u.password == password
}
func (u *user) resetPassword(password string) {
u.password = password
}
func main() {
a := user{name: "wang", password: "1024"}
a.resetPassword("2048")
fmt.Println(a.checkPassword("2048")) // true
}
\newline
错误处理
错误处理 在 go 语言里面符合语言习惯的做法是使用一个单独的返回值来传递错误信息。不同于 Java、c++使用的异常,go 语言的处理方式,能够很清晰地知道哪个函数返回了错误,并且能用简单的 if else 来处理错误。
以下代码样例中的findUser()函数中,可以在函数的返回值里加一个 error,就代表这个函数可能会返回错误。在函数实现的时候,需要 return 两个值,如果出现错误,就会return nil 和 error;没有错误则返回原本的结果和 nil。
package main
import (
"errors"
"fmt"
)
type user struct {
name string
password string
}
func findUser(users []user, name string) (v *user, err error) {
for _, u := range users {
if u.name == name {
return &u, nil
}
}
return nil, errors.New("not found")
}
func main() {
u, err := findUser([]user{{"wang", "1024"}}, "wang")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name) // wang
if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
fmt.Println(err) // not found
return
} else {
fmt.Println(u.name)
}
}
\newline
字符串操作
在标准库 strings 包里面有很多常用的字符串工具函数,比如
- contains:判断一个字符串里面是否有包含另一个字符串
- count:字符串计数
- index:查找某个字符串的位置
- join:连接多个字符串
- repeat:重复多个字符串
- replace:替换字符串
通过以下代码体会字符串操作:
package main
import (
"fmt"
"strings"
)
func main() {
a := "hello"
fmt.Println(strings.Contains(a, "ll")) // true
fmt.Println(strings.Count(a, "l")) // 2
fmt.Println(strings.HasPrefix(a, "he")) // true
fmt.Println(strings.HasSuffix(a, "llo")) // true
fmt.Println(strings.Index(a, "ll")) // 2
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
fmt.Println(strings.Repeat(a, 2)) // hellohello
fmt.Println(strings.Replace(a, "e", "E", -1)) // hEllo
fmt.Println(strings.Split("a-b-c", "-")) // [a b c]
fmt.Println(strings.ToLower(a)) // hello
fmt.Println(strings.ToUpper(a)) // HELLO
fmt.Println(len(a)) // 5
b := "你好"
fmt.Println(len(b)) // 6
}
\newline
字符串格式化
在标准库的 FMT 包里面有很多的字符串格式相关的方法,比如 printf,类似于 C 语言里面的 printf 函数。不同的是,在 go 语言里面的话,可以很轻松地用 %v 来打印任意类型的变量,而不需要区分数字或字符串;也可以用 %+v 打印详细结果,%#v 则更详细。
通过以下代码体会字符串格式化:
package main
import "fmt"
type point struct {
x, y int
}
func main() {
s := "hello"
n := 123
p := point{1, 2}
fmt.Println(s, n) // hello 123
fmt.Println(p) // {1 2}
fmt.Printf("s=%v\n", s) // s=hello
fmt.Printf("n=%v\n", n) // n=123
fmt.Printf("p=%v\n", p) // p={1 2}
fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
f := 3.141592653
fmt.Println(f) // 3.141592653
fmt.Printf("%.2f\n", f) // 3.14
}
\newline
JSON处理
go 语言里的JSON 操作非常简单,对于一个已有的结构体,可以什么都不做,只要保证每个字段的第一个字母是大写,也就是公开字段,这个结构体就能用 JSON.marshaler 去序列化,变成一个 JSON 的字符串。序列化之后的字符串也能够用 JSON.unmarshaler 去反序列化到一个空的变量里面。默认序列化出来的字符串风格是大写字母开头,而不是下划线。可以在后面用 json tag 等语法来去修改输出 JSON 结果里面的字段名。
通过以下代码体会JSON处理的写法:
package main
import (
"encoding/json"
"fmt"
)
type userInfo struct {
Name string
Age int `json:"age"`
Hobby []string
}
func main() {
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
buf, err := json.Marshal(a)
if err != nil {
panic(err)
}
fmt.Println(buf) // [123 34 78 97...]
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
buf, err = json.MarshalIndent(a, "", "\t")
if err != nil {
panic(err)
}
fmt.Println(string(buf))
var b userInfo
err = json.Unmarshal(buf, &b)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}
\newline
时间处理
在 go 语言里面最常用 time.now() 来获取当前时间,也可以用 time.date() 去构造一个带时区的时间,有很多方法来获取这个时间点的年月日时分秒,也能用 .sub 对两个时间进行减法,得到一个时间段,时间段又可以表示为对应的时分秒。在和某些系统交互时,经常会用到时间戳,可以用 .Unix 来获取时间戳。
通过以下代码体会时间处理的写法:
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
fmt.Println(t) // 2022-03-27 01:25:36 +0000 UTC
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2022-03-27 01:25:36
diff := t2.Sub(t)
fmt.Println(diff) // 1h5m0s
fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
if err != nil {
panic(err)
}
fmt.Println(t3 == t) // true
fmt.Println(now.Unix()) // 1648738080
}
\newline
数字解析
在 go 语言当中,关于字符串和数字类型之间的转换都在 STR conv 这个包下,这个包是 string convert 这两个单词的缩写。可以用 parselnt 或者 parseFloat 来解析一个字符串。可以用 Atoi 把一个十进制字符串转成数字,可以用 itoA 把数字转成字符串,如果输入不合法,那么这些函数都会返回error。
package main
import (
"fmt"
"strconv"
)
func main() {
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111
n, _ = strconv.ParseInt("0x1000", 0, 64)
fmt.Println(n) // 4096
n2, _ := strconv.Atoi("123")
fmt.Println(n2) // 123
n2, err := strconv.Atoi("AAA")
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}
\newline
进程信息
在 go 里面,可以用 os.Args 来得到程序执行的时候的指定的命令行参数。以下代码示例中,有编译的应该二进制文件,command,后面接abcd来启动,输出在 os.Argv 是一个长度为5的slice,第一个成员代表二进制自身的名字。可以用 os.Getenv 来读取环境变量,用 os.Setenv 写入环境变量。
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// go run example/20-env/main.go a b c d
fmt.Println(os.Args) // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
fmt.Println(os.Setenv("AA", "BB"))
buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // 127.0.0.1 localhost
}