这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记;主要学习了GO语言的init函数和包管理等机制,以及数据结构相关的内容。
init 函数 和 import 导包
一般情况下以main包存在的go文件作为主入口,以递归的方式 import 所需的包, 每个包执行结束后自动调用init() 函数(如果没有就调用默认的空init函数)
go 函数名的起名规则:
- 只有函数名首字母大写的函数,该函数才对外开放,否则只能包内使用
一般的导包方式
自定义包的小demo 注意go mod的问题
/* lib1 */
package lib1
import "fmt"
func init() {
fmt.Println("lib1 is working")
}
// lib1包提供的api
func Lib1Test() {
fmt.Println("api of the lib1")
}
/* lib2 */
package lib2
import "fmt"
func init() {
fmt.Println("lib2 is working")
}
// lib2包提供的api
func Lib2Test() {
fmt.Println("api of the lib2")
}
/* main */
package main
import (
"fmt"
"lib1" // 自定义的包
"lib2"
)
func main() {
lib1.Lib1Test()
lib2.Lib2Test()
}
import 匿名及别名导包
一个场景下,如果不想使用一个包的接口,但是想要调用该包的init函数,那么需要用到匿名import
下划线表示lib1包的别名,_ 表示这个匿名导入,之后就算不使用该包的任何接口函数,也不会报错
import (
"fmt"
_ "lib1"
mylib2 "lib2"
. "lib3" // 代表包名可省略,直接调用包中的函数(慎用)
)
指针
类似C/C++的指针,
package main
import (
"fmt"
)
func set(p *int) {
*p = 2
}
func main() {
var a int = 1
set(&a)
fmt.Println("a = ",a)
}
关键字 defer
机制类似于钩子函数,exit()结束之前调用所有的钩子函数
package main
import (
"fmt"
)
func main() {
// defer 关键字是指 经过该关键字修饰的语句直到函数结束前的最后一刻才执行
defer fmt.Println("end")
fmt.Println("hello")
fmt.Println("start")
}
tips1: defer 可以存在多个,以压栈的方式存放,所以最后调用时,以LIFO(后入先出)的方式调用
package main
import (
"fmt"
)
func main() {
// defer 关键字是指 经过该关键字修饰的语句直到函数结束前的最后一刻才执行
defer fmt.Println("a")
defer fmt.Println("b")
defer fmt.Println("c")
}
tips2: defer 是在当前函数生命周期结束之后才会出栈,被调用,所以defer的调用是在return之后的
package main
import (
"fmt"
)
func defFunc() int {
fmt.Println("defer")
return 0
}
func returnFunc() int {
fmt.Println("return")
return 0
}
func return_defer() int {
defer defFunc()
return returnFunc()
}
func main() {
return_defer()
}
slice
定长数组
Go 语言提供了定长数组,类似于c的数组,但是定长数组不管是在扩展方面,还是传参方面都没有优势,所以尽量少用,多用动态数组。
定长数组的声明与使用方式如下:
// 函数传参是值拷贝
package main
import (
"fmt"
)
// 定长数组只能传与之相匹配的形参(和c有区别)
func put_arr(arr [10]int) {
fmt.Println("func put_arr: ",arr)
arr[0] = 100
}
func main() {
// 固定长度的数组
var arr[10] int
for i := 0; i < len(arr); i++ {
fmt.Print(arr[i])
}
fmt.Println()
// 带初始化的定长数组声明
arr2 := [10]int {1,2,3,4}
for i := 0; i < len(arr2); i++ {
fmt.Print(arr2[i])
}
fmt.Println()
// for 的遍历
fmt.Println("range 遍历:")
for index,val := range arr2 {
fmt.Println(index,val)
}
// 查看数组的数据类型
fmt.Printf("arr types = %T\n,arr2 types = %T\n",arr,arr2);
put_arr(arr)
}
动态数组/slice
这是Go中使用的最常见的数据结构,类似于python的list(但是只能存储单类型),声明一个slice本质上是声明了一个指针,指向了分配的内存,还有类似list的切片操作。
// 基本使用
package main
import (
"fmt"
)
func put_arr(arr []int) {
// 下划线表示匿名
for _, val := range arr {
fmt.Println("val = ", val)
}
arr[0] = 99 // slice 是真的去数组存放位置修改
}
func main() {
// 动态数组/slice 的声明
// slice 是引用传递,实际上传的是指针
arr := []int {1,2,3,4}
fmt.Printf("arr types = %T\n",arr)
put_arr(arr)
for i := 0; i < len(arr); i++ {
fmt.Print(arr[i]," ")
}
fmt.Println()
}
slice 的声明方法
总共有四种:
- 带初始化的声明
- 声明一个空的slice,是一个切片类型,但并没有分配空间
- 利用make合并方法二的两种操作
- 通过 := 推导 , 这也是我们最常使用方式
动态的slice逃不掉的一环是当容量不够时如何扩增,Go默认的方式是以两倍的量去扩展,在项目开发中,如果提前知晓slice实际的使用量,去合理声明初始分配空间,可以有效提高代码性能。
package main
import (
"fmt"
)
func main() {
// 方法一: 带初始化的声明
slice1 := []int {1,2,3}
// %v 表示打印所有的详细信息
fmt.Printf("len = %d, slice = %v\n", len(slice1), slice1)
// 方法二,声明一个空的slice,是一个切片类型,但并没有分配空间
var slice2 []int
fmt.Println(len(slice2))
slice2 = make([]int, 3) // 开辟三个空间大小
fmt.Println(len(slice2))
// 方法三,合并方法二的两种操作
var slice3 []int = make([]int, 4)
fmt.Println(len(slice3))
// 方法四: 通过 := 推导 , 最常见方式
slice4 := make([]int, 5)
fmt.Println(len(slice4))
// 判断slice 是否有空间
// else 必须和if的括号写一行
var slice []int
if slice == nil {
fmt.Println("slice 是空切片")
} else {
fmt.Println("slice 不是空切片")
}
}
slice切片追加与截取
slice 有类似python 切片的操作,操作方式也和python类似,极大的方便了日常使用。 值得注意的是 ,通过切片的来的新slice并不会重新分配新空间,而是在底层数组中用指针指向,即原slice和新slice公用一块空间,就是所谓的浅拷贝,如果想要实现深拷贝,需要使用到copy函数。
package main
import (
"fmt"
)
func main() {
// 切片的长度和容量
var slice = make([]int, 3, 5)
fmt.Printf("len = %d, capcictiy = %d\n",len(slice), cap(slice))
fmt.Println(slice)
// append 增加元素
// capcictiy 成倍增长
slice = append(slice, 1,2,2) // 后面的参数类似cpp 的 ... 接收任意数量
fmt.Println("len = ", len(slice), "capcictiy = ", cap(slice), slice)
// 切片截取,类似python的切片,左闭右开
fmt.Println("slice[1:4] = ", slice[1:4])
// 截取全部
fmt.Println("slice[:] = ", slice[:])
// 截取前段
fmt.Println("slice[:4] = ",slice[:4])
// 截取后段
fmt.Println("slice [4:] = ",slice[4:])
// 创建新切片(浅拷贝)
// 但是 新切片 和 老切片指向的位置相同, 修改切片内容可能会导致两个切片都发生改变
slice_tmp := slice[1:5]
fmt.Println("new slice = ", slice_tmp)
slice[1] = 100
fmt.Println("slice = ", slice, "\nnew slice = ", slice_tmp)
// 深拷贝版,利用copy(dest,src)函数
slice2 := make([]int, 6)
copy(slice2,slice)
slice[2] = 99
fmt.Println("slice = ", slice, "\nnew slice = ", slice_tmp, "\ncopy slice = ", slice2)
}
map
声明方式
map 的底层是由hashtable实现,所以效率非常之高,类似于STL中的unordered_map,存取效率为O(1). 常见的一些使用方式如下:
package main
import (
"fmt"
)
func main() {
// 声明一个map类型, []内是key,括号外的是value
var myMap map[string]string
// 刚声明的map没有capcictiy
if myMap == nil {
fmt.Println("myMap is nil")
} else {
fmt.Println("myMap is not nil")
}
// 使用map前需要申请内存空间(map 没有cap 哎!)
myMap = make(map[string]string, 10)
// 或者合起来写,容量可以默认不写
myMap2 := make(map[string]string, 10)
myMap2["one"] = "java"
myMap2["two"] = "C++"
myMap2["three"] = "Golang"
fmt.Println(myMap2)
for key,value := range myMap2 {
fmt.Println("key = ", key, ",value = ", value)
}
// 第二种声明方式
myMap3 := map[int]string {
1 : "C",
2 : "php",
3 : "python",
}
fmt.Println(myMap3)
}
使用方式
tips: map 以指针方式传参,即他对应的变量名实际也只是个指针,指向了底层的hashtable
package main
import (
"fmt"
)
func main() {
myMap := map[int]string {
1 : "C",
2 : "php",
3 : "python",
}
fmt.Println(myMap)
// 遍历
for key,value := range myMap {
fmt.Println("key = ", key, ",value = ", value)
}
// 删除, 以key作为索引
delete(myMap,2)
fmt.Println("删除2后")
for key,value := range myMap {
fmt.Println("key = ", key, ",value = ", value)
}
// 修改
myMap[1] = "Go"
fmt.Println("修改1")
for key,value := range myMap {
fmt.Println("key = ", key, ",value = ", value)
}
// 添加
myMap[5] = "java"
fmt.Println("添加2")
for key,value := range myMap {
fmt.Println("key = ", key, ",value = ", value)
}
}