这是我参与「第五届青训营 」笔记创作活动的第1天。
简介
Go语言(又称Golang)是由Google开发的一种开源编程语言,于2009年推出。它是一种编译型语言,具有静态类型以及类似C语言的语法和性能。Go语言的主要特点有:简洁易懂的语法、高效的内存管理(有垃圾回收器)、优异的高并发性能、丰富的标准库和完善的工具链、对网络编程和大型系统开发有良好的支持,平缓的学习曲线使其广泛应用于后端开发、微服务、分布式系统以及物联网等领域。
入门
1. 开发环境
1.1 安装Golang
安装步骤很简单。
- 从官网下载对应系统的安装包。如果网络环境恶劣的话,也可以尝试从Golang中国的镜像下载。
- 运行安装包并按提示安装。
- 打开命令行工具,运行
go version命令检查Go是否安装成功。
1.2 配置集成开发环境
配置Go的集成开发环境可以使开发更加方便高效,推荐选用VSCode或Goland。
以VSCode为例,安装好VSCode之后只需要在插件市场中搜索安装Go插件,然后就可以开始愉快地写代码啦。
1.3 基于云的环境
大家还可以用gitpod的云环境来编写Go,只需要打开网站用github登录即可,十分的方便Nice!
2. 基础语法
2.1 第一个程序
学一门新语言,写的第一个程序当然是从输出Hello, world开始啦。
package main
import (
"fmt"
)
func main( ) {
fmt.Println("Hello, world!")
}
我们的第一个程序大概长这个样子。第一行的package main表明这个文件属于main包,main包也就是我们程序的入口包。第三行的import语句导入了Go的fmt包,这个包主要用来向屏幕输入输出以及格式化字符串。最后我们定义了main函数,用fmt包的Println方法输出了Hello, world!的字符串。
写完之后保存为hello.go就可以尝试运行啦,Go中构建代码的命令为go build <filename>,也可以用go run <filename>构建并运行。
$ go run hello.go
Hello, world!
$ go build hello.go
$ ./hello
Hello, world!
2.2 变量与常量
Go是一门强类型语言,每一个变量都有对应的类型。常见类型有整数、浮点数、字符串、布尔型等。
Go语言中定义变量有两种方式:
- 使用
var关键字定义变量,如var a int = 16。 - 使用缩写的
:=语法。如b := 3.1415926。
用var关键字时需要指定变量类型或初始值,编译器可以根据初始值自动推导相应变量类型,也可以由指定变量类型默认初始化为对应类型的零值;用:=定义变量需要注意,变量必须是没有被定义过的,否则会报错:
var a = "This is a string"
var b float // 初始化为0
var c bool = false
c := true //error: no new variables on left side of :=
还可以同时定义多个变量,如果变量值不重要可以用_来接受并忽略:
var name, country string = "John Doe", "US"
_, country := "John Doe", "US"
Go中字符串属于内置类型,可以使用+拼接,也可以使用==判断。运算符的使用和优先级参考c/c++。
常量需要使用关键字const,编译器可以自动由上下文推导常量的类型:
const s string = "const string"
const pi = 3.1415926
2.3 条件语句
Go中的if和else与c/c++类似,不同点是判断条件不需要用圆括号,里面允许声明一个变量(很方便),此外if和else后边必须跟着左花括号(c++选手表示不适):
if num := 9; num < 0 {
fmt.Println(num, "is negative.")
} else if num < 10 {
fmt.Println(num, "has one digit.")
} else {
fmt.Println(num, "has multiple digits.")
}
Go中的switch语句也与c/c++类似,不过同样的switch后边判断的变量不需要用圆括号。此外,每一个case语句不需要用break跳出,Go中执行完对应的case语句后会默认跳出switch结构。如果多个case对应同一段代码逻辑,可以用case var1, var2:的方式合并起来。
var grade string = "B"
var marks int = 90
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
2.4 循环语句
Go中只有一种for循环,有三种形式。
- 和C语言的
for一样:
// 与C语言一样,循环条件语句的三段可以任意省略
for j := 7; j < 9; j++ {
fmt.Println(j)
}
- 和C语言的
while一样:
i:=1
for i < 5 {
fmt.Println(i)
i=i+1
}
- 和C语言的
for(;;;)一样:
for {
fmt.Println("for loop")
break
}
类似的,Go语言也有break、continue语句,用法与C语言一致。
2.5 数组
Go中的数组是一种固定长度的元素序列,用来存储同一类型的数据。数组的长度是固定的,在定义时需要指定,如:
var balance1 [10]float32
var balance2 = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance[0] = 1000.0
var balances = [3][3]float32{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
fmt.Println(balance1, len(balance1))
需要注意的是,在Go语言中数组的长度是数组类型的一部分,因此[5]int和[25]int是不同的类型。由于数组的长度固定,在真实业务场景下很少用数组,更多的是用接下来介绍的切片。
2.6 切片
Go语言中的切片是对数组的一种抽象,是可变长度的数组,定义方式如下:
// 使用var关键字定义切片
var numbers []int
// 使用make函数来创建切片
b := make([]int, 3, 5)
当使用make函数定义切片时,需要指定切片的类型、长度,可以选择性指定切片容量。那什么是切片的长度和容量呢?切片实现的原理是存储了一个等于容量大小的数组、当前使用的长度以及指向数组的指针。往切片中添加元素时长度会相应增加,如果超出容量,就会扩容并返回一个新的切片。
切片追加元素需要使用append函数:
b = append(b, 5)
注意append函数是内建函数,不同于其他面向对象编程语言中的成员方法,因此只能用赋值的方式来追加(不适+1)。
最后Go中的切片拥有和python中一样的切片操作,但是不支持负索引:
fmt.Println(b[0:2])
fmt.Println(b[1:])
fmt.Println(b[:2])
2.7 map
在其他语言里,它可能叫做哈希表或者字典,是实际业务中使用最频繁的数据结构。
定义方式如下:
// 可以通过var关键字定义
var ages map[string]int
// 可以通过make函数创建
countries := make(map[string]int)
// 也可以通过字面值的形式创建
genders := map[string]string{"Alice": "Female", "Bob": "Male"}
查找值的方式与C++一致,如genders["Alice"],删除某个键值对用delete函数,如delete(genders, "Alice")。
2.8 range
对于数组、切片、字符串及map等,我们可以用range来快速便利,从而使代码优雅简洁,如:
nums := []int{1,2,3,4}
for i, n := range nums {
fmt.Println(i, n)
}
ages := map[string]int{"Alice": 31, "Bob", 25}
for name, age := range ages {
fmt.Println(name, age)
}
对于数组和切片,range会返回索引和对应的值,对于map会返回键值对,如果不需要索引可以用_接受并忽略。
2.9 函数
Go中的函数定义语法如下:
func function_name(arg1 type1, arg2, type2,...) return_type {
// 函数体
}
Go中变量类型都是写到后面的,return_type是返回类型,如果函数没有返回值可以省略返回类型。Go中原生支持返回多个值:
var exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok
}
在实际的业务代码中,几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个是错误信息。
2.10 指针
Go中的指针不同于C/C++,支持的操作有限,主要用来在函数中修改参数的值。
package main
import "fmt"
func modify(p *int) {
*p = 100
}
func main() {
x := 1
p := &x
fmt.Println(*p)
modify(p)
fmt.Println(*p)
}
2.11 结构体
结构体由一系列带类型的字段构成。定义示例如下:
type Person struct {
Name string
Age int
}
初始化结构体变量时需要传入每个字段的值:
p := Person{"John Doe", 30}
或者用键值对的方式,从而只对一部分字段进行初始化:
p := Person{Name: "John Doe", Age: 30}
q := Person{Name: "Bob"}
结构体也支持指针,通常将结构体变量指针作为函数参数,以减小大结构体变量的拷贝开销:
func information(p *Person) {
fmt.Println(p.Name, p.Age)
}
information(&p)
结构体方法是在结构体类型上定义的函数,与普通函数的区别在于结构体方法的func关键字多了接收器,就像函数的参数一样使其可以访问结构体的字段,如果接收器类型写为指针的话还可以修改字段的值,如:
type Person struct {
Name string
Age int
}
func (p Person) SayHello() {
fmt.Println("Hello, my name is", p.Name)
}
func (p *Person) ChangeName(name String) {
p.name = name
}
func main() {
p := Person{"John Doe", 30}
p.SayHello()
p.ChangeName("Bob")
p.SayHello()
}
2.12 错误处理
Go中习惯的做法是使用一个单独的返回值来传递错误信息。如果没有错误,就返回原本的结果和nil;如果出现了错误,就返回nil和对应的error。
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
result, err := divide(10, 2)
if err != nil {
fmt.Println(err)
}
fmt.Println(result)
2.13 字符串操作
在标准库strings包里有很多常用的字符串工具函数:
package main
import (
"fmt"
"strings"
)
func main() {
s:="hello"
fmt.Println(strings.Contain(s, "ll")) //true
fmt.Println(strings.Count(s, "l") //2
fmt.Println(strings.HasPrefix(s, "hel")) //true
fmt.Println(strings.HasSuffix(s, "llo")) //true
fmt.Println(strings.Index(s, "ll") //2
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) //he-llo
fmt.Println(strings.Repeat(s, 2)) //hellohello
fmt.Println(strings.Replace(s, "e", "E", -1)) //hEllo
fmt.Println(strings.Split("a-b-c", "-")) //[a b c]
fmt.Println(strings.ToLower(s)) //hello
fmt.Println(strings.ToUpper(s)) //HELLO
fmt.Println(len(s)) //5
}
2.14 字符串格式化
在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", s) //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
}
2.15 JSON处理
Go语言中内置了encoding/json包,提供了处理JSON数据的函数。使用json.Marshal函数可以将Go数据结构体编码为JSON,使用json.Unmarshal函数可以将JSON数据解码为Go数据结构体。其中结构体的各个字段名首字母应该大写,即保证各字段为公开字段。我们也可以在字段定义后面用标签tag语法来指定该字段在编码为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"}}
}
2.16 时间处理
Go语言标准库中提供了很多用于处理时间的函数和类型。常用的有time包,它提供了对时间的抽象表示,包括时间点、时间段和时区。time.Time类型表示一个时间点,可以通过调用time.Now()获取当前时间,也可以调用time.Data()来获取带时区的时间。可以使用format字符串对时间点进行格式化,也可以使用Parse函数将字符串解析为时间点。此外,还提供了时间差的计算、时区的处理和间隔的表示等功能。在与不同系统交互时,可以用.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
}
2.17 数字解析
Go中字符串与数字之间的转换可以用strconv这个包来完成。我们可以用ParseInt将字符串解析为整数,第二个参数为进制数,设为0时由字符串前缀自动推导,第三个参数为bit位大小。同样的,ParseFloat可以解析为浮点数,第二个参数也为bit位大小。我们也可以用Atoi将十进制字符串转为数字,或者用Itoa把数字转为字符串。
package main
import (
"fmt"
"strconv"
)
func main() {
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234
n, er := strconv.ParseInt("11111111", 10, 8)
fmt.Println(n, er) // 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
}
2.18 进程信息
Go的os包提供了与操作系统交互的一些函数。比如我们可以用os.Args来获取程序执行时的命令行参数,用os.Getenv、os.Setenv来获取和设定环境变量,也可以用os/exec包中的Command函数执行命令。
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"))
fmt.Println(os.Getenv("AA"))
buf, err := exec.Command("grep", "-E", `[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`, "/etc/hosts").CombinedOutput()
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // 127.0.0.1 localhost
}
进阶
好啦,现在我们对Go已经有了一个整体的认识,后面我们就可以做一些小的项目啦~(咕咕咕)