前言:
Go 语言是由 Google 公司推出的一款新的编程语言,作为谷歌的亲儿子,发展势头迅猛,各个大厂目前都在积极推进 Go 语言的使用。Go 是云计算、云原生、区块链等众多前沿领域的首推语言,目前流行的 Docker、Kubernetes 等开源项目也都是基于 Go 语言的。
Why GO?用 Go 的好处:
既然 Go 语言这么牛,当然要快快学起来鸭!
浏览器访问 Golang 官网 (https://go.dev) 按照官网提示下载并安装 Go 语言环境。安装过程非常简单,无脑点击下一步即可。
PS:
提到 IDE 就离不开大名鼎鼎的 JetBrains 了,如果你之前使用过它家出品的 Java 集成开发环境 IDEA,那么我强烈推荐尝试一下同样来自它家的 GoLand 作为 Go 语言 IDE。
GoLand 传送门:GoLand by JetBrains: More than just a Go IDE
此外,代码编辑器 VS Code 安装 Go 语言扩展以后也可以很好的支持 Go 语言开发,如果你喜欢更加轻量的开发环境,VS Code 也是一个不错的选择。
VS Code 传送门:Visual Studio Code - Code Editing. Redefined
现在让我们通过 Hello World 这个小例子来粗浅地了解一下 Go 语言。
package main
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
package main
表示这个文件属于 main 包的一部分,main 包是程序的入口包。import
用来导入包依赖,这里导入了标准库中的 fmt 包,fmt
包用来格式化输入输出。紧接着是 func main()
函数中的语句,调用了 fmt 包下的 Println()
方法在屏幕上打印 “Hello World”。
细心的你此时还会注意到 Go 的语法有点像 Python,在每条语句的末尾是不需要写分号 ;
的。
如果你使用的是 VS Code,可以在终端通过 go run
指令运行该程序:
$ go run example/01-hello/main.go
hello world
需要编译成可执行的二进制文件,可以使用 go build
指令:
$ go build example/01-hello/main.go
$ ./main
hello world
类似于 Python,Golang 不需要我们声明具体的变量或者常量类型,Golang 会根据上下文自动推导类型。
变量的声明主要有两种方式,第一种方式是 var + 变量名 = 值
,或者我们可以直接 变量名 := 值
的方式来声明一个变量。(如果有需要,也可以指定变量类型)
没有进行赋值,但是指明了是什么类型,Golang 会自动加上空值。
var a = "initial" // 字符串变量
var b, c int = 1, 2 // 多变量赋值并指定为整数类型
var d = true // 布尔类型变量
var e float64 // 类型为64位浮点数,一个空的变量
f := float32(e) // 强制转换类型 64位浮点->32位浮点
g := a + "foo" // 字符串拼接
需要注意的是 :=
这种方式是省略了 var
,但是这种方式能对新的变量使用,对已经声明的变量使用会报错。
常量的声明只需要把 var
改成 const
即可。
const s string = "constant"
const h = 500000000
const i = 3e20 / h
区别于 C 和 Java 语言,Golang 中的 if else
不需要加上括号 ()
,如果加上括号你的编辑器在保存时会自动帮你去掉括号。同时 if else
的花括号 {}
不可以省略,即使后面只跟着一条语句。
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
表达式中声明变量,这个变量的范围只在 if
中能够使用。
if score:=60; score>=60 {
fmt.Println("及格了")
}
Golang 中没有 while
与 do-while
循环,只能使用 for
循环。
for
可以像 C 语言的语法一样后面跟着三条语句,并且任意一条都可以省略,也后面可以什么都不写,表示一个死循环。同样的,for
循环也支持 continue
与 break
操作。
下面是一个 for
循环示例:
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
}
}
在 C++ 和 Java 语言中,switch
语句如果不显示的加上 break
语句,会从第一个匹配的 case
开始跑完下面所有的 case
,而 Golang 中的 switch
是不需要加 break
的。
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")
}
比如上面的代码执行完 case 2:
以后会直接跳出 switch
语句。
Golang 中的 switch
语句不仅支持数字,还可以使用字符串或者结构体,甚至用 switch
来取代任意的 if-else
语句。
// switch 替代 if-else 用来判断当前是上午还是下午
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
Golang 中使用 [数组长度]类型
表示一个数组,可以直接按照声明变量的方式来声明一个数组。
package main
import "fmt"
func main() {
var a [5]int // 声明一个长度为5的整形数组
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)
}
运行上面的示例得到的结果如下:
$ go run example/06-array/main.go
get: 0
len: 5
[1 2 3 4 5]
2d: [[0 1 2] [1 2 3]]
Go 语言切片是对数组的抽象。数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型 - 切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
可以声明一个未指定大小的数组来定义切片:var identifier []type
,或者通过 make()
函数创建切片。
// 声明切片的同时进行初始化赋值
good := []string{"g", "o", "o", "d"}
// 使用make函数创建一个初始长度为3的切片
s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
make([]T, len, capacity)
,len
是数组的长度并且也是切片的初始长度,capacity
为可选参数,用来指定切片容量。
使用 append()
函数在切片中追加新的元素。由于切片的底层还是数组,它包含了数组长度、容量和一个指向数组的指针,容量不够时会进行扩容并返回新的 slice,因此使用 append()
函数还需要将结果赋值给原变量。
s = append(s, "d")
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]
copy()
函数可以在两个 slice 之间拷贝数据:
c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]
类似于 Python,Golang 也支持切片操作:
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]
s[2:5]
表示取出切片中第 2 个位置到第 5 个位置的元素(不包括第 5 个位置)。
map 是一种无序的键值对的集合。map 最重要的一点是通过 key
来快速检索数据,key
类似于索引,指向数据的值。
map 类似于 C++ 或是 Python 中的哈希与字典,不过 Go 中的 map 是完全无序的,遍历的顺序与插入顺序和字典序都没有关系。
可以使用内建函数 make()
也可以使用 map
关键字来定义 map:
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
如果不初始化 map,那么就会创建一个 nil map
,nil map
不能用来存放键值对。
下面是一个 map 使用示例:
package main
import "fmt"
func main() {
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
fmt.Println(m)
fmt.Println(len(m))
fmt.Println(m["one"])
fmt.Println(m["unknow"])
// 获取k-v对时可以在后面加一个ok来检查该key是否存在:
r, ok := m["unknow"]
fmt.Println(r, ok)
// 删除指定的k-v对:
delete(m, "one")
// 创建map同时进行初始化:
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
}
运行结果如下:
$ go run example/07-slice/main.go
get: c
len: 3
[a b c d e f]
[a b c d e f]
[c d e]
[a b c d e]
[c d e f]
[g o o d]
Go 语言中 range
关键字用于 for
循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在 map 中返回 key-value 对。
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
}
}
for i, num := range nums
中的 i
是切片 nums
的索引下标,num
是 nums
中对于下标的值,如果不需要使用下标,可以使用下划线 _
代替。
for k, v := range m
遍历集合 map 中的键值对,第一个 k
是键,第二个 v
是值。
运行上面的示例得到的结果如下:
$ go run example/09-range/main.go
index: 0 num: 2
9
a A
b B
key a
key b
Golang 中使用 func 函数名(参数列表) 返回值类型 {}
来声明一个函数。
默认情况下,Go 语言使用的是值传递,函数在调用时参数是拷贝复制的,即在调用过程中不会影响到实际参数。
Golang 和其他语言的函数有很大的不同,Go 原生支持在函数中返回多个值。在实际的开发中,函数一般都会返回多个值,第一个值是我们真正要返回的结果,第二个值是错误信息。
package main
import "fmt"
// 定义函数:返回两整形相加的结果
func add(a int, b int) int {
return a + b
}
// 参数类型相同可以简写:
func add2(a, b int) int {
return a + b
}
// 返回多个值的函数,第一个返回值v,第二个返回值ok
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)
v, ok := exists(map[string]string{"a": "A"}, "a")
fmt.Println(v, ok)
}
运行上面的示例得到的结果如下:
$ go run example/10-func/main.go
3
A true
和 C/C++ 一样,Golang 也初步支持了指针,相比于 C++,Go 的指针只支持有限的操作,通常用来修改参数。
一个指针变量指向了一个值的内存地址,声明格式:var var_name *var-type
。
var ip *int /* 指向整型 */
var fp *float32 /* 指向浮点型 */
下面是一个指针的使用示例:
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)
add2ptr(&n)
fmt.Println(n)
}
由于 Golang 的函数参数是原来的拷贝,在 add2(n int)
函数中对变量 n
的自增操作并不会生效。要想自增的效果作用在变量 n
上,add2ptr(n *int)
函数中使用了指针类型作为参数,调用时为了确保类型匹配,入参要加上 &
符表示取变量 n
的地址。
结构体是带类型的字段的集合,使用 type 结构体名 struct{}
定义一个结构体,使用点 .
来访问结构体中的字段。
下面是结构体的使用示例:
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
}
运行上面的代码得到的结果如下:
$ go run example/12-struct/main.go
{wang 1024} {wang 1024} {wang 1024} {wang 1024}
false
false
结构体方法类似于 Java 中的类成员函数,我们可以将上一个例子中的 checkPassword()
从一个普通函数改写成结构体方法,并使用 .
符号来调用它。
改写方法如下:
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 中的错误处理习惯使用一个单独的返回值来处理错误信息。这种方式可以很清晰的知道哪个函数出现了错误,并且可以使用 if-else
对错误进行处理。
下面是一个错误处理示例:
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)
}
}
在函数的返回值类型中加一个 error
,代表这个函数可能会返回错误,调用函数对返回的 error
信息进行判断就可以确定函数是否出错。
该示例的运行结果如下:
$ go run example/14-error/main.go
wang
not found
Golang 提供了丰富的标准库供我们使用,使用时只需在 import()
中导入对应的包即可。
标准库中的 strings 包提供了非常多的字符串工具函数,常用的方法参考下面的表格。
strings 包下的函数 | 描述 |
---|---|
Contains() | 判断一个字符串中是否包含另一个字符串 |
Count() | 统计指定子串在字符串中的出现次数 |
Index() | 查找指定子串的位置 |
Join() | 对多个字符串进行拼接 |
Repeat() | 将字符串重复指定次数 |
Replace() | 替换字符串中的内容 |
Split() | 将字符串按照给定的字符进行分割 |
此外还可以使用内置函数 len()
来获取字符串的长度,对于中文,一个中文可能对应多个字符。
下面是 strings 包下函数的使用示例:
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
}
"encoding/json"
包提供了 Json 的处理工具,可以实现结构体与 Json 格式的相互转换。
"json"
包中的 Marshal()
方法用来将结构体序列化成 Json 字符串,该方法返回的是一个 byte 数组,要想打印成字符串需要强制类型转换。使用 Unmarshal() 方法可以将 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"}}
}
在上面的示例中,序列化后的字符串默认的风格是大写字母开头,如果需要改成小写风格,可以在结构体的字段后面加上一个 json tag,比如上面代码中的 Age
字段后面指定了 json 字符串的字段名。
Golang 中最常用的是使用 time.Now()
获取当前的时间,也可以使用 time.Date()
方法构造指定时期的时间。
下面是 time 包下常用方法的使用示例:
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
}
在 "strconv"
包中封装了字符串与数字的转换工具。
ParseInt()
方法接收三个参数,第一个参数是指定的字符串,第二个参数是进制(传入 0 会自动推测字符串表示的进制),第三个参数是转换精度。ParseFloat()
方法接收两个参数,字符串与转换精度,该方法返回转换后的浮点数。
Atoi()
方法只接收一个字符串参数,用来快速地将字符串转成十进制地数字。同样的也可以使用 Itoa()
方法将一个数字转换成对应的字符串。
下面是 "strconv"
包中函数的使用示例:
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
}