GO语言基础 | 青训营笔记

84 阅读14分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

1. Go 语言特点

  1. 高性能,高并发。性能和 C、C++ 相当,采用协程模型,可轻松实现上万并发逻辑。
  2. 语法简单,学习曲线平缓。风格类似于C语言,并在其基础上进行了简化。
  3. 拥有大量功能完善、质量可靠的标准库。
  4. 完善的工具链。在编译、代码格式化、错误检查、帮助文档、包管理、代码补充提示等都有对应的工具。
  5. 静态链接。编译结果默认都是静态链接的,只需要拷贝编译后的唯一可执行文件。
  6. 快速编译。
  7. 跨平台。
  8. 垃圾回收。无需考虑内存分配释放。

2. Go 基础语法

2.1 Hello World

第一个程序:

package main                    // main 包,函数入口
import "fmt"                    // 导入 fmt 标准格式包
/* 主函数 */
func main() {
    fmt.Println("Hello World")  // 打印
}

2.2 变量

介绍:

  1. 变量表示内存中的一个存储区域。
  2. 该区域有自己的 变量名类型
  3. 变量声明赋值方式。

变量声明:

/* 写法1 */
var num int			// 变量声明
num = 12			// 变量赋值
/* 写法2 */
var num int = 12		// 变量声明并赋值
/* 写法3 */
num := 12			// 变量声明并赋值, 省略 var

多变量声明:

/* 写法1 */
var name, age = "tom", 18   // 多变量声明并赋值
/* 写法2 */
name, age := "tom", 18      // 多变量声明并赋值, 省略 var

全局变量声明:

var (
    name = "tom"
    age = 18
)
  1. 变量赋值时双方类型必须一致。
  2. 变量在同一作用域不可重名。
  3. 变量若没有赋初值,则会有默认值,如 0、空串、nil。

2.3 if else

基本语法:

if 条件表达式1 {
	执行代码块1
} else if 条件表达式2 {
	执行代码块2
} else {
	执行代码块3
}

注意要点:

  1. 当条件表达式为 true 时,就会执行 {...} 的代码。
  2. 添加表达式建议不加中括号,但是加了也不会报错。
  3. 注意 {} 是必须有的,即使只有一行也不能省略。
  4. 支持在条件表达式中定义变量。
if age := 20; age > 18 {
	...
}
  1. 条件表达式中不能写赋值语句,只能写返回值为布尔类型的表达式。
if b = false {   // 语句错误,不可写赋值语句。
	...
}

2.4 for

基本语法:

for 循环变量初始化; 循环条件; 循环变量迭代 {
	循环操作(语句)
}

注意要点:

  1. 循环条件是一个返回布尔值的表达式。
  2. 可以写为类似 C语言 while 写法。
i := 0		// 变量初始化
for i <= 10 {
	i++ 	// 循环执行语句
}
  1. 可以去掉循环条件实现死循环,配合 break 使用。
for {
	...   // 执行块
	if 判断条件 {
		break
	}
}
  1. for range 可遍历字符串,数组等。通过此方法可以遍历中文字符串。for index, val := range str {...}
// 打印英文正常,打印中文乱码
for i := 0; i < len(str); i++ {
	fmt.Println(str[i])
}
// 打印英文正常,打印中文正常
for index, val := range str {
	fmt.Println(val)
}
// 打印英文正常,打印中文正常 	
str2 = []rune(str)     // (切片)
for i := 0; i < len(str2); i++ {
	fmt.Println(str[i])
}

2.5 switch

基本语法:

switch 表达式 {
case 表达式1, 表达式2, ...:
	语句块1
case 表达式3, 表达式4, ...:
	语句块2
default:
	语句块
}

注意要点:

  1. case 后是一个表达式。(常量、变量、表达式、有返回值函数)
  2. case 后的各个表达式数据类型,必须和 switch 表达式数据类型一致。
  3. case 后可以带多个表达式,使用逗号间隔。
switch 表达式 {
case 表达式1, 表达式2, ...:
	// 代码内容
}
  1. case 后表达式若为常量,则不可重复出现。
  2. case 后不需要带 break,程序匹配到一个 case 会执行对应代码块并退出,default 在匹配不到 case 时执行。
  3. default 语句不是必须的。
  4. switch 后可不带表达式,这是可当作 if else 使用。
switch {
case 表达式1:
	...
case 表达式2:
	... 
}
  1. switch 后可直接声明定义一个变量,分号结束。
switch num := 12; {
	...
}
  1. switch 穿透,fallthrought,默认只能穿透一层。
switch num {
case num == 1:
	fmt.Println(num)
	fallthrough
case num == 2:
	fmt.Println(num)
}
  1. type switch,switch 可用于判断某个 interface 变量中实际指向的变量。
func TypeJudge(items... interface{}) {
   for _, x := range items {
	switch x.(type) {
	case bool:
	    fmt.Printf("bool类型, 值为%v\n", x)
	case float32, float64:
	    fmt.Printf("float类型, 值为%v\n", x)
	case int, int64:
	    fmt.Printf("int类型, 值为%v\n", x)
	case nil:
	    fmt.Printf("nil类型, 值为%v\n", x)
	case string:
	    fmt.Printf("string类型, 值为%v\n", x)
	case Student:
	    fmt.Printf("Student类型, 值为%v\n", x)
	default:
	    fmt.Println("未知类型")
	}
    }
}

2.6 数组

介绍:

  1. 数组的地址可以通过 &intArr 来获取。
  2. 数组的第一个元素的地址,就是数组的首地址。
  3. 数组各个元素的地址间隔,取决于元素的类型大小。
var intArr [3]int	// 默认初始化为 [0, 0, 0]
fmt.Println(intArr)	// 取出整个数组
fmt.Println(intArr[1])	// 取出某元素
  1. 初始化数组的方式。
// 方式一
var numArr [3]int = [3]int{1, 2, 3}
// 方式二
var numArr = [3]int{5, 6, 7}
// 方式三
var numArr = [...]int{8, 9, 10}
// 方式四
var numArr = [...]int{1:800, 0:900, 2:999}
  1. 数组遍历方式。
// 方式一
for i := 0; i < len(numArr); i++ {
	...
}
// 方式二
for index, value := range numArr {
	...
}

注意要点:

  1. 数组是多个相同数据类型的组合,且长度固定,不能动态变化。
  2. var arr []int 中 arr 是一个 slice 切片。
  3. 数组中的元素可以为任何数据类型,包括值类型引用类型,但不可混用。
  4. 数组创建后,如果没有赋值,有默认值(零值)。
  5. 数组为值传递,默认进行值拷贝;如果想在其他函数中修改数组值,可使用引用传递。
  6. 长度是数组类型的一部分,在传递函数参数时,需要考虑数组的长度。
func demo(numArr *[3]int) {	// 引用传递
	(*numArr)[0] = 30
}

2.7 切片

介绍:

  1. 切片是数组的一个引用,在进行传递时,遵循引用的机制。
  2. 切片包含三个部分,指向数组的指针 point *[len]int,切片的长度 len,切片的容量 cap,长度和容量可以通过 len(),cap() 获取。
  3. 切片使用的方式。
// 方式一:数组事先存在,程序员可见
var arr = [...]int{1, 2, 3, 4, 5}
var slice = arr[1:3]	// 取出的索引为 1, 2,不包含 3
// 方式二:使用 make 创建,由切片维护数组,程序员不可见
var slice []int = make([]int, 4, 10)
// 方式三:直接指定具体数组
var slice []string = []string{"tom", "jerry", "hello"}
  1. 切片遍历方式。
var arr = [...]int{10, 20, 30, 40, 50}
slice := arr[1:4]
// 方式一
for i := 0; i < len(slice); i++ {
	fmt.Println(slice[i])
}
// 方式二
for _, value := range slice {
	fmt.Println(value)
}

注意要点:

  1. 切片初始化时,下标不能越界,范围在 [0-len(arr)] 之间,可以动态增长。
var slice = arr[0:end]  	    // 可写为 arr[:end]
var slice = arr[start:len(arr)]     // 可写为 arr[start:]
var slice = arr[0:len(arr)] 	    // 可写为 arr[:]
  1. 切片定义后本身是空的,需要引用到数组或者 make 一片空间。
  2. 切片可以继续切片,当一个切片发生变化,与其相关的切片都会变化。
slice2 := slice[1:3]
  1. 使用 append() 可以对切片进行动态追加元素或切片。
var slice []int = []int{10, 20, 30}
slice = append(slice, 40, 50)
slice = append(slice, slice2...)
  1. 使用 copy() 对切片进行拷贝。
var slice []int = []int{1, 2, 3, 4, 5}
var slice2 = make([]int, 10)
var slice3 = make([]int, 3)
copy(slice2, slice)	// [1, 2, 3, 4, 5 ,0, 0, 0, 0, 0]
copy(slice3, slice)	// [1, 2, 3]
  1. string 底层是 []byte 类型,因此也可进行切片。
str := "hello@world"
slice := str[6:]	// "world"
  1. string 不可通过索引改变值,但可以通过切片修改。
arr := []rune(str)	// 字符串转为切片
arr[0] = 'z'		// 修改数组
str = string(arr)	// 切片转为字符串

2.8 map

介绍:

  1. 声明 map 是不分配内存的,初始化需要 make 函数。
// 方式一
var info map[string]string
info = make(map[string]string, 10)
// 方式二
info := make(map[string]string)
// 方式三
info := map[string]string {
	"on1" : "tom",
	"on2" : "jerry",
}
  1. delete() 函数可删除某键值对。
delete(info, "on1")
  1. 键值对的查找。
val, ok := info["on1"]
if ok {
	fmt.Println("找到,值为%v",val)
} else {
	fmt.Println("未找到")
}
  1. map 的遍历,采用 for range 结构遍历。
for k, val := range info {
	fmt.Pringln(k, val)
}
  1. map 切片。
// 声明一个 map 切片 
var stu = []map[string]string
stu = make([]map[string]string, 2)
stu[0] = make(map[string]string, 2)
stu[0]["name"] = "tom"
stu[0]["age"] = "18"
stu[1] ...
newStu := map[string]string {
	"name" = "jerry",
	“age” = "20",
}
stu = append(stu, newStu)
  1. map 排序。
var keys []int
for k, _ := range info {    // 把 key 取出来
	keys = append(keys, k)
}
sort.Ints(keys)	            // 将 key 排序 
for _, k := range keys {
	fmt.Println(k, info[k])	// 按排序后的结果输出
}

注意要点:

  1. map 是引用类型,一个函数接收 map 并修改后,会直接修改原来的 map。
  2. map 的容量达到后,再想增加元素会自动扩容,动态增长键值对。
  3. map 的值经常使用 struct 类型。
type Stu struct {
	Name string
	Age int
}
students := make(map[string]Stu, 10)
stu1 := Stu{"tom", 18}
stu2 := Stu{"jery", 20}
students["on1"] = stu1
students["on2"] = stu2

2.9 函数

介绍:

  1. 函数用法。
func 函数名(形参列表) (返回值列表) {
	执行语句...
	return 返回值列表
}
  1. 函数形参列表可能是多个,返回值列表也可以是多个。
  2. 形参列表和返回值列表的数据类型可以是值类型和引用类型。
  3. 函数首字母大写,可以被本包文件和其他包文件使用,类似 public。
  4. 函数的变量是局部的,函数外不可使用。
  5. 基本数据类型和数组默认是值传递。
  6. 若希望函数内的变量可以修改函数外的变量,可以传入变量的地址。
func demo(num *int) {
	*num += 1
}
num2 := 10
demo(&num2)
  1. Go 中不支持函数重载。
  2. Go 中函数也是一个数据类型,可以赋值给一个变量,该变量就是一个函数类型的变量。
func getSum(n1 int, n2 int) int {
	return n1 + n2
}
a := getSum
// 都为 func(int, int) int
fmt.Println("a的类型为%T, getSum的类型为%T", a, getSum)	
res := a(10, 10)
  1. Go 中函数可以作为形参,并且调用。
func myFun(funvar func(int, int) int, num1 int, num2 int) {
	return funvar(num1, num2)
}
res2 := myFun(getSum, 50, 60)
  1. Go 支持自定义数据类型,可简化数据类型定义。
type myInt int	// 给 int 取了别名,虽然都是 int 类型,但 Go 认为 myInt 和 int 是两种类型
var num1 myInt = 20
var num2 int
num2 = int(num1)	// 需要类型转换
type myFunType func(int, int) int // myFunType 是 func(int, int) int 的别名
func myFun(funvar myFunType, num1 int, num2 int) {...}
  1. 支持对函数返回值命名。
func getSumAndSub(n1 int, n2 int) (sum int, sub int) {
	sum = n1 + n2
	sub = n1 - n2
	return
}
a, b := getSumAndSub(1, 2)
  1. 使用 _ 标识符,忽略返回值。
a, _ := getSumAndSub(1, 2)
  1. go 支持可变参数,args... 是 slice 切片。
func sum(n1 int, args... int) int {
	sum := n1
	for i := 0; i < len(args); i++ {
		sum += args[i]	// 取出 args 切片中的值
	}
}

2.10 结构体

声明初始化:

type Student struct {
	Name string
	Age int
}
// 方式一:直接声明
var stu Student
stu.Name = "tom"
stu.Age = 18
// 方式二:{}
var stu Student = Student{"tom", 20}
// 方式三:
var stu *Student = new(Student)
(*stu).Name = "tom"	// go 中可省略为 stu.Name = "tom"
stu.Name = "tom"
(*stu).Age = 22 
// 方式四:
var stu *Student = &Student{}
(*stu).Name = "tom"	// go 中可省略为 stu.Name = "tom"
stu.Name = "tom"
(*stu).Age = 24

注意事项:

  1. 结构体中的所有字段在内存中是连续的。
  2. 结构体是用户单独定义的类型,和其他类型进行强转时,需要有完全相同的字段。
type A struct {
	Num int
}
type B struct {
	Num int
}
var a A
var b B
a = A(b)
  1. 结构体进行 type 重新定义时,go 认为其是新的数据类型,但是相互之间可以强转。
type Student struct {
	Name string
	Age int
}
type Stu Student
var stu1 Student
var stu2 Stu
stu2 = Stu(stu1)
  1. struct 的每个字段上,可以写一个 tag,该 tag 可以通过反射机制获取,常用于序列化和反序列化。
type Student struct {
	Name string `json:"name"`
	Age int	`json:"age"`
}
var stu Student = Student{"tom", 18}
// 将结构体序列化,序列化后 Name->name,Age->age
jsonStr, err := json.Marshal(stu)	

2.11 方法

声明:

type Student struct {
	Name string
	Age int
}
func (stu Student) demo() {
	fmt.Println(stu.Name)
}
var stu1 Student = Student{"tom", 18}
stu1.demo()

注意事项:

  1. 结构体类型时值类型,在方法调用中,采用值拷贝方式。
  2. 如果希望在方法中改变结构体变量的值,可以采用结构体指针的方式。
  3. 数据类型如 int,struct,自定义类型等都可以使用方法。
  4. 方法名首字母小写只能在本包使用,首字母大写可以在其他包访问。
  5. 如果一个类型实现了 String() 方法,那么 fmt.Println() 默认会调用这个变量的 String() 进行输出。
func (stu *Student) String() string {
	str := fmt.Printf("Name:%v, Age:%v", stu.Name, stu.Age)
	return str 
}
stu := Student{"tom", 18}
fmt.Println(&stu)

2.12 错误处理

介绍:

  1. go 中引入的处理方式为 defer、panic、recover。
func test() {
    defer func() {
	  // recover 内置函数,可以捕获到异常
	  if err := recover(); err != nil {	
	  fmt.Println("err = ", err)
    }()
    num1 := 10
    num2 := 0
    res := num1 / num2
}
func main() {
    test()
    for {
	  fmt.Println("main()下的代码")
	  time.Sleep(time.Second)
    }
}
  1. 自定义错误,使用 errors.New 和 panic 内置函数。
  • errors.New("错误说明"),会返回一个 error 类型的值。
  • panic 内置函数,接收一个 interface{} 类型的值作为参数,输出错误信息并退出程序。
## 示例:函数读取配置文件 init.conf 的信息
## 如果文件名传入不正确,返回一个自定义错误
func readConf(name string) (err error) {
    if name == "config.ini" {
        return nil
    } else {
        return errors.New("读取文件错误")
    }
}
func test() {
    err := readConf("config2.ini")
    if err != nil {
        panic(err)  // 输出错误,并终止程序
    }
    fmt.println("test()继续")
}
func main() {
	test()
	fmt.println("main()继续")
}

2.13 字符串

介绍

  1. 统计字符串的长度。
str := "hello世界"
num = len(str)		# num = 11go 采用 utf-8 编码,汉字占 3 个字节
  1. 字符串遍历,同时处理有中文的问题,采用 rune 切片。
str2 := []rune(str)
for i := 0; i < len(str2); i++ {
	fmt.Println(str2[i])
}
  1. 字符串转整数。
n, err := strconv.Atoi("123")
if err != nil {
	fmt.Println("转换错误", err)  
} else {
	fmt.Println("转换成功", n)
}
  1. 整数转字符串。
str = strconv.Itoa(123)
  1. 字符串转 []byte。
var bytes = []byte("hello go")
  1. []byte 转字符串。
str = string([]byte{97, 98, 99})	// abc
  1. 将10进制转 2,8,16进制,返回对应字符串。
str = strconv.FormatInt(123, 2)
str = strconv.FormatInt(123, 16)
  1. 查找字符串中是否包含某子串。
b = strings.Contains("seafood", "fo")	// true
  1. 统计字符串中包含子串的个数。
n = strings.Count("eat seafood", "ea")	// 
  1. 不区分大小写比较。
strings.EqualFold("abc", "ABc")	// true
  1. 返回子串第一次出现的 index,没有则返回 -1。
index = strings.Index("seafood", "ea")	// 1
  1. 返回子串最后一次出现的 index,没有则返回 -1。
index = strings.LastIndex("eat seafood", "ea")	// 5
  1. 将指定的子串替换成另外的子串,n 可以指定替换几个, -1 为替换所有。
str = strings.Replace("eat seafood", "ea", "hh", -1)	// hht shhfood
  1. 按照某个字符,将一个字符才分为字符串数组。
str = strings.Split("hello,world,OK", ",")	// [hello, world, OK]
  1. 字符串大小写转换。
str = strings.ToLower("goLang Hello")
str = strings.ToUpper("goLang Hello")
  1. 将字符串两端空格去掉。
str = strings.TrimSpace("  goLang Hello  ")	// "goLang Hello"
  1. 将字符串两边指定的字符去掉。
strings.Trim("! he!llo ! ", " !")	// "he!llo"
strings.TrimLeft("! he!llo ! ", " !")	// "he!llo !"
strings.TrimRight("! he!llo ! ", " !")  // "! he!llo"
  1. 判断字符串是否以指定的字符串开头或结尾。
b = strings.HasPrefix("http://123.345.com", "http")
b = strings.HasSuffix("files.jpg", ".jpg")

2.14 json 处理

struct 序列化

type Student struct {
	Name string `json:"name"`		// 使用 tag 标签
	Age int	`json:"age"`
}
stu := Student{"tom", 18}
data, err := json.Marshal(&stu)
if err != nil {
	fmt.Println("序列化错误 err=", err)
}
fmt.Println("序列化结果为", string(data))

map 序列化

var a map[string]interface{}
a = make(map[string]interface{})
a["Name"] = "tom"
a["Age"] = 18
data, err := json.Marshal(&a)
if err != nil {
	fmt.Println("序列化错误 err=", err)
}
fmt.Println("序列化结果为", string(data))

struct 反序列化

type Student struct {
	Name string
	Age int
}
str := "{\"name\":\"tom\",\"age\":18}"
var stu Student
err := json.Unmarshal([]byte(str), &stu)
if err != nil {
	fmt.Println("反序列化错误 err=", err)
}
fmt.Println("反序列化结果为", string(data))

map 反序列化

str := "{\"Age\":18,\"Name\":\"tom\"}"
var a map[string]interface{}
err := json.Unmarshal([]byte(str), &stu)
if err != nil {
	fmt.Println("反序列化错误 err=", err)
}
fmt.Println("反序列化结果为", string(data))

2.15 时间处理

介绍

  1. 获取当前时间。
// 2023-01-17 13:56:12.724042518 +0000 UTC m=+0.000046782
now := time.Now()	
  1. 获取其他日期信息。
fmt.Println(now.Year())
fmt.Println(now.Month())
fmt.Println(now.Day())
  1. 格式化日期时间。
// 方法一
fmt.Println(“时间:%d-%d-%d\n”, now.Year(), now.Month(), now.Day())
// 方法二
// 这个时间时官方规定的,不可更改
fmt.Println(now.Format("2006/01/02 15:04:05"))	
  1. 时间的常量。
const (
   Nanosecond  Duration = 1
   Microsecond          = 1000 * Nanosecond
   Millisecond          = 1000 * Microsecond
   Second               = 1000 * Millisecond
   Minute               = 60 * Second
   Hour                 = 60 * Minute
)
// 如获取100毫秒
t := 100 * time.Millisecond
  1. 休眠。
for i := 0; ;i++ {
	fmt.Println(i)
	time.Sleep(100 * time.Millisecond)
	if i == 10 {
		break
	}
}
  1. 通过 unix 或 UnixNano 获取随机种子,生成随机数。
rand.Seed(now.UnixNano())
rand.Intn(10)