文章和代码已经归档至【Github仓库:https://github.com/timerring/backend-tutorial 】或者公众号【AIShareLab】回复 go 也可获取。
简介
什么是 Go 语言
- 高性能、高并发
- 语法简单、学习曲线平缓
- 丰富的标准库(很多情况下不需要借助第三方库就可以完成基础功能的开发)
- 完善的工具链(无论是编译,代码检查,补充提示等等都有对应的工具,还内置了完整的单元测试框架,性能检测等等)
- 静态链接(所有的编译结果都是静态链接的,只需要拷贝编译后的可执行文件,不加东西就可以直接运行,部署方便快捷。对比 java 需要一个庞大的 jre 才可以运行)
- 非常快速的编译
- 跨平台(可以在多种设备上运行,还有交叉编译特性,无需配置交叉编译环境)
- 垃圾回收(专注业务逻辑,无需考虑内存的分配与释放)
哪些公司使用 Go 语言
首先字节跳动已经全面拥抱了 go 语言,公司内部有上万个微服务使用 golang 来编写,不久前也开源了 GORPC 框架 KiteX。腾讯、百度、美团、滴滴、深信服、平安、OPPO、知乎、去哪儿、360、金山、微博、bilibili、七牛、PingCAP 等公司也在大量使用 Go 语言。
国外 Google Facebook 等公司也在大量使用 Go 语言。
从业务维度看过语言已经在云计算、微服务、大数据、区块链、物联网等领域蓬勃发展。然后在云计算、微服务等领域已经有非常高的市场占有率
Docker、Kubernetes、Istio、etcd、prometheus 几乎所有的云原生组件全是用 Go 实现的。
为什么转型 Go 语言
- 最开始公司的后端业务主要是 web 后端,早期团队非 Java 背景,C++不太适合在线 Web 业务。
- 最开始的服务都是 python ,从 2014 年开始,随着业务体量的增长,python 遇到一些性能问题。
- 一些团队初步尝试使用了 Go,发现入门很简单,开发效率高,性能也比较好。
- Go 语言的开发和部署非常简单,顺带解决了之前 python 带来的很头疼的依赖库版本问题。
- —些业务尝到甜头之后,后面开始公司级大力推广,诞生了公司内部的基于 golang 的 rpc 和 http 框架。
Go 入门
Go 安装
访问 go.dev/ 或访问 studygolang.com/dl
Go 开发环境
- 基于云的开发环境:gitpods.IO 在线编程环境,需要 GitHub 账号
- 集成开发环境:Vs Code(免费)或者 Goland(收费)
基础语法
// package main 代表这个文件属于main包的一部分, main包也就是程序的入口包。
package main
// 导入了标准库里面的 FMT 包。这个包主要是用来往屏幕输入输出字符串、格式化字符串。
import (
"fmt"
)
// import 下面是 main 函数,main 函数的话里面调用了 fmt.Println 输出 helloworld要运行这个程序的话,我们就直接 go run heloworld.go。
func main() {
fmt.Println("hello world")
}
如果想编译成二进制的话,可以在 go build
来编译,编译完成之后直接 ./helloword
就可以运行在 FMT 包里面还有很多的函教来做不同的输入输出格式化工作。大家可以在编辑器里面把鼠标悬浮在你的代码上,就可以看到每一个函数的文档。
你也可以进入 pkg.Go.Dev
,后面加你的包名比如 FMT 然后就能看到这个包的在线文档,可以从里面去挑选你需要的函数来使用。
变量
package main
import (
"fmt"
"math"
)
func main() {
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))
}
下面我们来看第二个例子,关于变量类型。
go 语言是一门强类型语言,每一个变量都有它自己的变量类型。常见的变量类型包括字符串整数浮点型、布尔型等,go 语言的字符串是内置类型,可以直接通过加号拼接,也能够直接用等于号去比较两个字符串。
go 语言的变量的声明,在 go 语言里变量的声明有两种方式
- 一种是过
var name string=" "
这种方式来声明变量,声明变量的时候,一般会自动去推导变量的类型。如果有需要,也可以显示写出变量类型。 - 另一种声明变量的方式是使用
变量 := 等于值
。
常量的话就是把 var 改成 const,值在一提的是 go 语言里面的常量,它没有确定的类型,会根据使用的上下文来自动确定类型。
if-else
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")
}
}
Go 语言里面的 if else 写法和 C 或者 C++类似。不同点是 if 后面没有括号,且Golang 里面的 if,它必须后面接大括号,不能像 C 或者 C++—样,直接把 if 里面的语句同一行。
循环
package main
import "fmt"
func main() {
i := 1
for {
fmt.Println("loop")
break
}
for j := 7; j < 9; j++ {
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
}
}
在 go 里面没有 while 循环、do while 循环,只有唯一的一种 for 循环。最简单的 for 循环就是在 for 后面什么都不写,代表一个死循环。可以用 break 或者 continue 来跳出或者继续循环。
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 里面写条件分支
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}
在 switch 后面的那个变量名,并不是要括号。这里有个很大的一点不同的是,在 c++里面, switch case 如果不显示加 break 的话会然后会继续往下跑完所有的 case,在 go 语言里面的话是不需要加 break 的。
相比 C 或者 C++, go 语言里面的 switch 功能更强大。可以使用任意的变量类型,甚至可以用来取代任意的 if else 语句。你可以在 switch 后面不加任何的变量,然后在 case 里面写条件分支。这样代码相比你用多个 if else 代码逻辑会更为清晰。
数组
package main
import "fmt"
func main() {
// 这里的话是一个可以存放 5 个 int 元素的数组 A
var a [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)
}
数组就是一个具有编号且长度固定的元素序列。对于一个数组,可以很方便地取特定索引的值或者往特定索引取存储值,然后也能够直接去打印一个数组。不过,在真实业务代码里面,我们很少直接使用数组,因为它长度是固定的,我们用的更多的是切片。
切片
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]
}
切片不同于数组可以任意更改长度,然后也有更多丰富的操作。可以用 make 来创建一个切片,可以像数组一样去取值, 使用 append 来追加元素。
注意 append 的用法的话,你必须把 append 的结果赋值为原数组。
因为 slice 的原理实际上是它有一个它存储了一个长度和一个容量,加一个指向一个数组的指针,在你执行 append 操作的时候,如果容量不够的话,会扩容并且返回新的 slice。
Slice 此初始化的时候也可以指定长度。
Slice 拥有像 python 一样的切片操作,比如这个代表取出第二个到第五个位置的元素,不包括第五个元素。不过不同于 python,这里不支持负数索引。
map
package main
import "fmt"
func main() {
// 用 make 来创建一个空 map,这里会需要两个类型。
// 第一个是那个 key 的类型,这里是 string 另一个是 value 的类型,这里是 int。
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 从里面删除键值对。
delete(m, "one")
m2 := map[string]int{
"one": 1, "two": 2}
var m3 = map[string]int{
"one": 1, "two": 2}
fmt.Println(m2, m3)
}
Map 是实际使用过程中最频繁用到的数据结构。
Golang 的 map 是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。
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
}
}
对于一个 slice 或者一个 map 的话,我们可以用 range 来快速遍历,这样代码能够更加简洁。Range 遍历的时候, 对于数组会返回两个值,第一个是索引,第二个是对应位置的值。如果我们不需要索引的话,我们可以用下划线来忽略。
函数
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
}
这个是 Golang 里面一个简单的实现两个变量相加的函数。Golang 和其他很多语言不一样的是,变量类型是后置的。
Golang 里面的函数原生支持返回多个值。在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个值是一个错误信息。
指针
package main
import "fmt"
func add2(n int) {
n += 2
}
func add2ptr(n *int) {
*n += 2
}
func main() {
n := 5
add2(n)
fmt.Println(n) // 5
add2ptr(&n)
fmt.Println(n) // 7
}
Go 里面也支持指针。当然,相比 C 和 C++里面的指针,支持的操作很有限。指针的一个主要用途就是对于传入参数进行修改。
例如上面这个函数试图把一个变量+2。但是单纯像上面这种写法其实是无效的。因为传入函数的参数实际上是一个持贝, 那也说这个+2,是对那个拷贝进行了+2,并不起作用。如果我们需要起作用的话,那么我们需要把那个类型写成指针类型,那么为了类型匹配,调用的时候会加一个&符号。
结构体
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
}
结构体的话是带类型的字段的集合。
比如这里 user 结构包含了两个字段, name 和 password。我们可以用结构体的名称去初始化一个结构体变量,构造的时候需要传入每个字段的初始值。也可以用这种键值对的方式去指定初始值,这样可以只对一部分字段进行初始化。
结构体方法
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
}
在 Golang 里面可以为结构体去定义一些方法。会有一点类似其他语言里面的类成员函数。比如这里,我们把上面一个例子的checkPassword
的实现,从一个普通函数,改成了结构体方法。这样用户可以像a.checkPassword ("x )这样去调用。D 具体的代码修改,就是把第一个参数,加上括号,写到函数名称前面。
在实现结构体的方法的时候也有两种写法,一种是带指针,一种是不带指针。如果带指针的话,就可以对这个结构体去做修改。如果不带指针的话,实际上操作的是一个拷贝,你就无法对结构体进行修改。
错误处理
package main
import (
"errors"
"fmt"
)
type user struct {
name string
password string
}
// 在函数里面,我们可以在那个函数的返回值类型里面,后面加一个 error,就代表这个函数可能会返回错误。
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)
}
}
错误处理在 go 语言里面符合语言习惯的做法就是使用一个单独的返回值来传递错误信息。
不同于 Java 自家家使用的异常。Go 语言的处理方式,能够很清晰地知道哪个函数返回了错误,并且能用简单的 if else 来处理错误。
在函数里面,我们可以在那个函数的返回值类型里面,后面加一个 error,就代表这个函数可能会返回错误。
那么在函数实现的时候, return 需要同时 return 两个值,要么就是如果出现错误的话,那么可以 return nil
和一个 error
。如果没有的话, 那么返回原本的结果和 nil。
字符串操作
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
}
在标准库 strings 包里面有很多常用的字符串工具函数,比如 contains 判断一个字符串里面是否有包含另一个字符串,count 字符串计数, index 查找某个字符串的位置。Join 连接多个字符串 repeat 重复多个字符串 replace 替换字符串。
字符串格式化
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
}
在 go 语言里,你可以很轻松地用 %v
来打印任意类型的变量,而不需要区分数字字符串。你也可以用 %+v
打印详细结果, %#v
则更详细。
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"}}
}
go 语言里面的 JSON 操作非常简单,对于一个已有的结构体,我们可以什么都不做,只要保证每个字段的第一个字母是大写,也就是是公开字段。那么这个结构体就能用 JSON. Marshaler 去序列化,变成一个 JSON 的字符串。
序列化之后的字符串也能够用 JSON. Unmarshaler 去反序列化到一个空的变量里面。
这样默认序列化出来的字符串的话,它的风格是大写字母开头,而不是下划线。我们可以在后面用 json tag 等语法来去修改输出 JSON 结果里面的字段名。
时间处理
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
}
下面是时间处理,在 go 语言里面最常用的就是 time.Now ()
来获取当前时间,然后你也可以用 time.Date
去构造一个带时区的时间,构造完的时间。上面有很多方法来获取这个时间点的年月日小时分钟秒,然后也能用点 sub 去对两个时间进行减法,得到一个时间段。时间段又可以去得到它有多少小时,多少分钟、多少秒。
在和某些系统交互的时候,我们经常会用到时间戳。那您可以用 .UNIX()
来获取时间戳。time.format
,time.parse
用于解析时间,具体可以参考文档。
数字解析
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
}
下面我们来学习一下字符串和数字之间的转换。在 go 语言当中,关于字符串和数字类型之间的转换都在 strconv
这个包下,这个包是 string convert 这两个单词的缩写。
我们可以用 parselnt 或者 parseFloat 来解析一个字符串。Parseint 参数
我们可以用 Atoi 把一个十进制字符串转成数字。可以用 itoA 把数字转成字符串。如果输入不合法, 那么这些函数都会返回 error。
进程信息
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// 比如我们编译的一个二进制文件,command。后面接 abcd 来启动,输出就是os.argv会是一个长度为5的 slice ,第一个成员代表:进制自身的名字。
// 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
}
在go 里面,我们能够用os.argv
来得到程序执行的时候的指定的命令行参数。
我们可以用os.getenv
来读取环境变量。