# go学习资料 **Repository Path**: oncetarget/go-learning ## Basic Information - **Project Name**: go学习资料 - **Description**: 个人学习笔记。 - **Primary Language**: Go - **License**: WTFPL - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2023-06-15 - **Last Updated**: 2023-06-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README `注:该博客仅为个人学习笔记。` # go学习资料 ### 介绍 学习笔记 ![avatar](./images/go-route.png) ### 基础知识 #### go 语言关键字、标识符、数据类型、变量、流程控制、函数、数组、闭包 ##### 关键字 - break - 使用break关键字可以终止循环并继续执行其余代码 - case - 这是`switch`构造的一种形式。我们在切换后指定一个变量。 - chan - `chan` 关键字用于定义通道。在`执行`中,允许您同时运行并行代码。 - const - `const`关键字用于标量值引入名称,常量 - continue - `continue`使用关键字可以返回到`for`循环的开头,跳过当前循环 - default - `default`语句是可选的,在`switch`语句中使用case和default.如果值与表达式不匹配,则跳到默认值。 - defer - 关键字`defer`用于推迟执行功能,直到周围的功能执行为止,如果是在函数中最后执行 - else - 如果`if`条件为假,则执行else下的语句 - fallthrough - 在`switch`语句中使用该关键字。当我们使用该关键字时,将执行下面的case条件 - for - `for`开始for循环 - func - `func`关键字声明一个函数 - go - `go`关键字触发一个goroutine(异步处理),该例程由golang运行时管理 - goto - `goto`关键字可无条件跳转至带标签的语句 - if - `if`语句用于检查循环内的特定条件。 - import - `import` 关键字用于导入软件包。 - interface - `interface`关键字用于指定方法集。方法集时一种类型的方法列表。 - map - `map`关键字定义map类型。映射是键值对的无序集合。 - package - `package`关键字代码在包中分组为一个单元。类似代码文件在文件夹中的统一包名。 - range - `range`关键字可以迭代列表(map或者数组)。遍历循环(map或者数组)。 - return - go允许您将返回值用作变量,并且可以为此目的使用return关键字。 - select - `select`关键字使goroutine在同步通信操作期间等待处理。 - struct - `struct`是字段的集合。我们可以在字段声明后使用struct关键字,定义结构体。 - switch - `switch`语句用于启动循环并在块内使用if-else逻辑。 - type - `type`我们可以使用`type`关键字引入新的结构类型。 - var - `var`关键字用于创建go语言的变量 ##### 标识符 > 标识符是指go语言对各种函数、方法、变量等命名时使用的字符序列,标识符由若干个字母、下划线`_`、和数字组成,并且第一个字符必须是字母。 > 下划线`_`是一个特殊的标识符,称之为空白标识符,它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),它赋值给它的值都将被抛弃,因此不可以使用`_`作为变量给其他变量进行赋值或运算。 > 变量、类型、函数或者代码内标识符的名称不能重复。 - 标识符命名需要遵守以下规则: - 由 26个字母、0-9、`_`组成 - 不能以数字开头,必须以字母 - go 语言中严格区分大小写 - 标识符不能包含空格 - 不能以系统关键字作为标识符,比如:`break`、`if`等 - 命名标识符还需要注意以下几点: - 标识符命名尽量简短并且有意义,让人容易理解 - 不能和标准库中的包名重复 - 为变量、函数、常量命名时采用驼峰命名法,例如 stuName、getVal ##### 数据类型 > bool - 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。 > 数字类型 - 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 > 字符串类型 - string > 错误类型 > 派生类型: - 指针类型 pointer - 数组类型 - 结构化类型 struct - 通道类型 channel - 函数类型 - 切片类型 - 接口类型 interface - map类型 - 指针类型 - 类型指针:允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。 - 切片指针:由指向起始元素的原始指针、元素数量和容量组成。 (`变量、指针和地址三者的关系是:每个变量都拥有地址,指针的值就是地址`) - 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下: - 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。 - 指针变量的值是指针地址。 - 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。 > *操作符作为右值时,意义是取指针的值,作为左值时,也就是放在赋值操作符的左边时,表示 a 指针指向的变量。其实归纳起来,*操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。 - 数组类型 > 数组被称为array,就是一个由若干相同类型的元素组成的序列。 > 注意:数组的长度是数组类型的一部分。只要类型声明中数组长度不同,即使两个数组类型的元素类型相同,它们还是不同的类型。列入:[2]string和[3]string > 数组类型的下标都是整数 - 结构化类型 struct > 基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物所有或者部分的属性时,go语言提供了一种自定义的数据类型,可以封装多个基础数据类型,这种数据类型叫做结构体。struct - 通道类型 channel > 通道`channel`是一种特殊的类型 > 在任何时候,`同时只能有一个goroutine`访问通道进行发送和获取数据。goroutine之间通过通道就可以通信。 通道像一个传送带或者队列,总是遵循`先进先出`的规则,保证收发数据的顺序。 - channel本身就是一个队列,先进先出 - 线程安全,不需要加锁 - 本身是有类型的,string、int等,如果要存多种类型,则定义成interface类型 - channel是引用类型,必须make之后才能使用,一旦make,它的容量就确定了,不会动态增加!!!它和map,slice不一样 特点: - 一旦初始化容量,就不会改变了 - 当写入数据容量已满时,不可再写入,取空时,不可以取。 - 发送将持续阻塞直到数据被接受 >把数据往通道中发送时,如果接收方一直没有接收,那么发送操作将持续阻塞。 - 接收将持续阻塞直到发送方发送数据 >如果接收方接收时,通道中没有发送方发送的数据,接收方也会发送阻塞,直到发送方发送数据为止 - 每次接收一个元素 >通道一次只能接收一个元素。 - 函数类型 > 可以把函数作为一种变量,用`type`去定义它,那么这个函数类型就可以作为值传递 > type calsulTest func(int,int) //声明一个函数类型 - 切片类型 > 数据结构是切片,动态数组,其长度并不固定,可以在切片中追加元素,它会在容量不足时自动扩容。 > 切片长度可以随着元素数量的增长而增长(`但不会随着元素的数量减少而减少`) > 切片数据类型是有如下结构体表示的 - Data 是指向数组的指针 - Len 是当前切片的长度 - Cap 是当前切片的容量大小,即Data数组的大小 > 切片占用的内存空间=切片中元素大小 X 切片容量 > 切片自动扩容,扩容后新切片的容量将会是原切片容量的2倍,如果还是不足以容纳新元素,则按照同样的操作继续扩容,直到新容量不小于原长度与追加的元素数量之和。 > 切片扩容是生成容量更大的切片,把原有元素和新元素一并copy到新切片中。 - 接口类型 interface > interface是一种类型,从它的定义可以看出用了`type`关键字,准确的来说interface是一种`具有一组方法的类型`. > interface被多种类型实现时,需要区分interface的变量时那种储存类型的值,go需要用断言方式。go 可以使用 comma, ok 的形式做区分 value, ok := em.(T):em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示是否为该断言的类型 T。 - map类型 > map是一堆键值对的未排序集合,类似Python中字典的概念,它的格式为map[keyType]valueType,是一个key-value的hash结构。map的读取和设置也类似slice一样,通过key来操作,只是slice的index只能是int类型,而map多了很多类型,可以是int,可以是string及所有完全定义了==与!=操作的类型。 - map 声明 (其中:key为键类型,value为值类型) ``` var map变量名 map[key] value ``` - 注意事项 - map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取。 - map的长度是不固定的,也就是和slice一样,也是一种引用类型。 - 内置的len函数同样适用于map,返回map拥有的key的数量。 - map的值可以很方便的修改,通过重新赋值即可。 `关键字: map make delete` ##### 变量 > var 声明语句可以创建一个特定类型的变量,然后给变量附加名称,并且设置初始值 ``` var 变量名称 类型 = 表达式 var aa string = "golang" ``` > 简洁声明 `:` ``` aa := "golang" ``` > 初始化一组变量 ``` i,j := 0,1 ``` - 注意 - `:=`是一个变量声明语句 - `=`是一个变量赋值操作 ##### 指针 > 一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名,比如叫x的变量,但是还有很多变量始终以表达式方式引入,例如x[i]或者x.f变量。所有这些表达式一般都是读取一个变量的值,除非它们是出现在赋值语句的左边,这种时候是给对应变量赋予一个新的值。 > 一个指针的值是另外一个变量的地址。一个指针对应变量在内存中的储存位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。 > 如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是*int,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时*p表达式对应p指针指向的变量的值。一般*p表达式读取指针指向的变量的值,这里为int类型的值,同时因为*p对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。 ```x := 1 p := &x // p, of type *int, points to x fmt.Println(*p) // "1" *p = 2 // equivalent to x = 2 fmt.Println(x) // "2" ``` > 任何类型的指针的零值都是nil.如果p指向某个有效变量,那么p != nil测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。 ``` var x, y int fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false" ``` ##### 流程控制 - if else (分支结构) > 关键字 if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号{}括起来的代码块,否则就忽略该代码块继续执行后续的代码。如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行,if 和 else 后的两个代码块是相互独立的分支,只能执行其中一个。 ``` if condition { // do something } else { // do something } ``` - for (循环结构) > 与多数语言不同的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构,关键字 for 的基本使用方法与C语言和 C++ 中非常接近: ``` sum := 0 for i := 0; i < 10; i++ { sum += i } ``` > `for 中的结束语句——每次循环结束时执行的语句` > 在结束每次循环前执行的语句,如果循环被 break、goto、return、panic 等语句强制退出,结束语句不会被执行。 - for range (键值循环) > for range 结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道(channel),for range 语法上类似于其它语言中的 foreach 语句,一般形式为: ``` for key, val := range coll { fmt.Println(key,val) } ``` 通过 for range 遍历的返回值有一定的规律: - 数组、切片、字符串返回索引和值。 - map 返回键和值。 - 通道(channel)只返回通道内的值。 - switch case 语句 > switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行,示例代码如下: ``` var a = "hello" switch a { case "hello": fmt.Println(1) // 输出 1 case "world": fmt.Println(2) default: fmt.Println(0) } ``` > 一分支多值 (多个条件对应一个值) ``` var a = "mum" switch a { case "mum", "daddy": fmt.Println("family") } ``` > 分支表达式 ``` var r int = 11 switch { case r > 10 && r < 20: fmt.Println(r) } ``` `跨越 case 的 fallthrough——兼容C语言的 case 设计` > 在Go语言中 case 是一个独立的代码块,执行完毕后不会像C语言那样紧接着执行下一个 case,但是为了兼容一些移植代码,依然加入了 fallthrough 关键字来实现这一功能 ``` var s = "hello" switch { case s == "hello": fmt.Println("hello") fallthrough case s != "world": fmt.Println("world") } // 输出 hello world ``` - goto语句——跳转到指定的标签 > goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助,使用 goto 语句能简化一些代码的实现过程。 ``` package main import "fmt" func main() { for x := 0; x < 10; x++ { for y := 0; y < 10; y++ { if y == 2 { // 跳转到标签 goto breakHere } } } // 手动返回, 避免执行进入标签 return // 标签 breakHere: fmt.Println("done") } // 标签只能被 goto 使用,但不影响代码执行流程,此处如果不手动返回,在不满足条件时,也会执行第 24 行代码。 // 输出 y=2时候跳到标签breakHere,输出done // 使用场景:打印日志等 ``` ##### 函数 > 函数的基本组成为:关键字 func、函数名、参数列表、返回值、函数体和返回语句,每一个程序都包含很多的函数,函数是基本的代码块。 > 当函数执行到代码块最后一行}之前或者 return 语句的时候会退出,其中 return 语句可以带有零个或多个参数,这些参数将作为返回值供调用者使用,简单的 return 语句也可以用来结束 for 的死循环,或者结束一个协程(goroutine)。 - 三种类型的函数: - 普通的带有名字的函数 - 匿名函数或者 lambda 函数 - 方法 - 普通函数声明(定义) > 函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。 ``` func aa(a int) int { return a } fmt.Println(aa(123)) // 123 // func 函数名(形式参数列表)(返回值列表){ 函数体 } ``` - 函数变量——把函数作为值保存到变量中 > 函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f,并将一个函数名为 fire() 的函数赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数 ``` package main import ( "fmt" ) func fire() { fmt.Println("fire") } func main() { var f func() f = fire f() } // 输出 fire ``` ``` package main import ( "fmt" ) func fire() int { return 24 } func main() { f := func() int { return 0 } f = fire fmt.Println(f()) } // 输出 24 // 函数变量 f 进行函数调用,实际调用的是 fire() 函数。 ``` - 匿名函数 > 匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成。 - 定义一个匿名函数 ``` f := func(aa int) { fmt.Println(aa) return } // 使用f()调用 f(24) // 输出 24 ``` > 匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。 - 匿名函数用作回调函数 ``` package main import ( "fmt" ) // 遍历切片的每个元素, 通过给定函数进行元素访问 func visit(list []int, f func(int)) { for _, v := range list { f(v) } } func main() { // 使用匿名函数打印切片内容 visit([]int{1, 2, 3, 4}, func(v int) { fmt.Println(v) }) } // 输出 1 2 3 4 // 使用 visit() 函数将整个遍历过程进行封装,当要获取遍历期间的切片值时,只需要给 visit() 传入一个回调参数即可。 ``` - defer(延迟执行语句) > defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。 > 逆序执行(类似栈,即后进先出) ``` package main import ( "fmt" ) func main() { fmt.Println("defer begin") // 将defer放入延迟调用栈 defer fmt.Println(1) defer fmt.Println(2) // 最后一个放入, 位于栈顶, 最先调用 defer fmt.Println(3) fmt.Println("defer end") } // 输出 // defer begin // defer end // 3 // 2 // 1 ``` - 代码的延迟顺序与最终的执行顺序是反向的 - 延迟调用是在 defer 所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。 `使用延迟执行语句在函数退出时释放资源` > 处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。 > defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。 - 使用延迟并发解锁 > 函数中并发使用 map,为防止竞态问题,使用 sync.Mutex 进行加锁 ``` var ( // 一个演示用的映射 valueByKey = make(map[string]int) // 保证使用映射时的并发安全的互斥锁 valueByKeyGuard sync.Mutex ) // 根据键读取值 func readValue(key string) int { // 对共享资源加锁 valueByKeyGuard.Lock() // 取值 v := valueByKey[key] // 对共享资源解锁 valueByKeyGuard.Unlock() // 返回值 return v } // 实例化一个 map,键是 string 类型,值为 int。 // map 默认不是并发安全的,准备一个 sync.Mutex 互斥量保护 map 的访问。 // readValue() 函数给定一个键,从 map 中获得值后返回,该函数会在并发环境中使用,需要保证并发安全。 // 使用互斥量加锁。 // 从 map 中获取值。 // 使用互斥量解锁。 // 返回获取到的 map 值。 ``` 使用 defer 语句对上面的语句进行简化 ``` func readValue(key string) int { valueByKeyGuard.Lock() // defer后面的语句不会马上调用, 而是延迟到函数结束时调用 defer valueByKeyGuard.Unlock() return valueByKey[key] } ``` - 使用延迟释放文件句柄 > 文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源 ``` func fileSize(filename string) int64 { f, err := os.Open(filename) if err != nil { return 0 } // 延迟调用Close, 此时Close不会被调用 // 注意,不能将这一句代码放在第 4 行空行处(err 上方/open打开文件下方),一旦文件打开错误,f 将为空,在延迟语句触发时,将触发宕机错误。 defer f.Close() info, err := f.Stat() if err != nil { // defer机制触发, 调用Close关闭文件 return 0 } size := info.Size() // defer机制触发, 调用Close关闭文件 return size } ``` - 递归函数 > 所谓递归函数指的是在函数内部调用函数自身的函数。 构成递归需要具备以下条件: - 一个问题可以被拆分成多个子问题 - 拆分前的原问题与拆分后的子问题除了数据规模不同,但处理问题的思路是一样的 - 不能无限制的调用本身,子问题需要有退出递归状态的条件 `注意:编写递归函数时,一定要有终止条件,否则就会无限调用下去,直到内存溢出。` - 宕机(panic)——程序终止运行 > 系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。 > 宕机不是一件很好的事情,可能造成体验停止、服务中断,就像没有人希望在取钱时遇到 ATM 机蓝屏一样,但是,如果在损失发生时,程序没有因为宕机而停止,那么用户将会付出更大的代价,这种代价可以是金钱、时间甚至生命,因此,宕机有时也是一种合理的止损方法。 > 当宕机发生时,程序会中断运行,并立即执行在该 goroutine(可以先理解成线程)中被延迟的函数(defer 机制),随后,程序崩溃并输出日志信息,日志信息包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息。 - 手动触发宕机 ``` package main func main() { panic("crash") } ``` - 在宕机时触发延迟执行语句 > 当 panic() 触发的宕机发生时,panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行过的 defer 语句依然会在宕机发生时发生作用. ``` package main import "fmt" func main() { defer fmt.Println("宕机后要做的事情1") defer fmt.Println("宕机后要做的事情2") panic("宕机") } // 输出 // 宕机后要做的事情2 // 宕机后要做的事情1 // 宕机 ``` `宕机前,defer 语句会被优先执行` - 宕机恢复(recover)——防止程序崩溃 > Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。 panic 和 recover 的关系,panic 和 recover 的组合有如下特性: - 有 panic 没 recover,程序宕机。 - 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。 - Test功能测试函数 > 完善的测试体系,能够提高开发的效率,当项目足够复杂的时候,想要保证尽可能的减少 bug,有两种有效的方式分别是代码审核和测试,Go语言中提供了 testing 包来实现单元测试功能。 > 要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀,例如: ``` func TestXxx( t *testing.T ){ //...... } ``` 编写测试用例有以下几点需要注意: - 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中 - 测试用例的文件名必须以`_test.go`结尾 - 需要使用 import 导入 testing 包 - 测试函数的名称要以Test或Benchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数 - 单元测试则以`(t *testing.T)`作为参数,性能测试以`(t *testing.B)`做为参数 - 测试用例文件使用`go test`命令来执行,源码中不需要 `main()` 函数作为入口,所有以`_test.go`结尾的源码文件内以Test开头的函数都会自动执行。 `testing 包提供了三种测试方式,分别是单元(功能)测试、性能(压力)测试和覆盖率测试。` - 单元(功能)测试 - 性能(压力)测试 - 覆盖率测试 ### 面试题目 #### 基础入门 ##### 数据类型 - nil切片和空切片是一样的吗? > 不是,nil切片和空切片最大的区别是`指向数组引用地址不一样的`。 ``` // 切片数据结构 type SliceHeader struct { Data uintptr //引用数组指针地址 Len int // 切片的目前使用长度 Cap int // 切片的容量 } ``` > nil切片的Data,数据指向的是0,`nil空切片引用数组指针地址为0(无指向任何实际地址)` > 空切片的Data,数据指向的是固定的地址,`所有的空切片指向数组引用地址都一样的,是一个固定的值` - 字符串转成byte数组,会发生内存拷贝吗? > 会,只要是发生类型强转换都会发生内存拷贝。 ``` // StringHeader 是字符串在go的底层结构 type StringHeader struct { Data uintptr Len int } ``` - 翻转含有中文、数字、英文字母的字符串 > 翻转"您好帅哥abc啊哥帅" ``` package main import ( "fmt" "strings" ) func main() { str := "您好帅哥abc啊哥帅" strArr := strings.Split(str,"") for i,j := 0,len(strArr)-1; i < j; i, j = i+1, j-1{ val := strArr[i] strArr[i] = strArr[j] strArr[j] = val } fmt.Println(strArr) // 输出 [帅 哥 啊 c b a 哥 帅 好 您] } ``` - 拷贝大切片与小切片的代价一样吗? > 一样的,所有切片的大小都相同:三个字段 ``` type SliceHeader struct { Data uintptr // 指向切片底层数组的指针,储存空间 Len int // 切片长度 Cap int // 容量 } ``` > 大切片和小切片的区别:len和cap的值大小,如果发生拷贝,本质上就是拷贝上面的三个字段,将一个 slice 变量分配给另一个变量只会复制三个机器字。所以 拷贝大切片跟小切片的代价应该是一样的。 - map不初始化使用会怎么样? > map不初始化在运行程序中会panic: assignment to entry in nil map ``` var mapx map[string]string mapx["1"] = "1" fmt.Println(mapx) // panic: assignment to entry in nil map ``` - map不初始化长度和初始化长度的区别 > map切片数据结构是三个字段构成,长度都是一样的0 > map不初始化为 nil值的map,不能用来存放键值对 - map承载多大,大了怎么办? (还需补充) > 跟内存相关,map有自动扩容机制:每次map进行更新或者新增的时候,会先通过以上函数判断一下load factor。来决定是否扩容。 - map可以边遍历边删除吗? > map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。 > 上面说的是发生在多个协程同时读写同一个 map 的情况下。 如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。但是,遍历的结果就可能不会是相同的了,有可能结果遍历结果集中包含了删除的 key,也有可能不包含,这取决于删除 key 的时间:是在遍历到 key 所在的 bucket 时刻前或者后。 > 一般而言,这可以通过读写锁来解决:`sync.RWMutex` > 读之前调用 `RLock()` 函数,读完之后调用 `RUnlock()` 函数解锁;写之前调用 `Lock()` 函数,写完之后,调用 `Unlock()` 解锁。 > 另外,`sync.Map` 是线程安全的 map,也可以使用。 - 怎么判断一个数组是否已经排序? > sort包中实现了3种基本的排序算法:插入排序.快排和堆排序 ``` type Interface interface { Len() int // Len 为集合内元素的总数 Less(i, j int) bool //如果index为i的元素小于index为j的元素,则返回true,否则返回false Swap(i, j int) // Swap 交换索引为 i 和 j 的元素 } ``` > 任何实现了 sort.Interface 的类型(一般为集合),均可使用该包中的方法进行排序。这些方法要求集合内列出元素的索引为整数。 - func Float64s(a []float64) //Float64s将类型为float64的slice a以升序方式进行排序 - func Float64sAreSorted(a []float64) bool  //判定是否已经进行排序func Ints(a []int) - func Ints(a []int) //Ints 以升序排列 int 切片。 - func IntsAreSorted(a []int) bool    //IntsAreSorted 判断 int 切片是否已经按升序排列。 - func IsSorted(data Interface) bool IsSorted 判断数据是否已经排序。包括各种可sort的数据类型的判断. - func Strings(a []string)//Strings 以升序排列 string 切片。 - func StringsAreSorted(a []string) bool//StringsAreSorted 判断 string 切片是否已经按升序排列。 > 使用上述函数就用判断该数组是否已经排序 ``` package main import ( "fmt" "sort" ) //定义interface{},并实现sort.Interface接口的三个方法 type IntSlice []int func (c IntSlice) Len() int { return len(c) } func (c IntSlice) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c IntSlice) Less(i, j int) bool { return c[i] < c[j] } func main() { a := IntSlice{1, 3, 5, 7, 2} b := []float64{1.1, 2.3, 5.3, 3.4} c := []int{1, 3, 5, 4, 2} fmt.Println(sort.IsSorted(a)) //false if !sort.IsSorted(a) { sort.Sort(a) } if !sort.Float64sAreSorted(b) { sort.Float64s(b) } if !sort.IntsAreSorted(c) { sort.Ints(c) } fmt.Println(a)//[1 2 3 5 7] fmt.Println(b)//[1.1 2.3 3.4 5.3] fmt.Println(c)// [1 2 3 4 5] } ``` - 普通map如何不用锁解决线程问题? > 在内置的 sync 包中(Go 1.9+)也有一个线程安全的 map,通过将读写分离的方式实现了某些特定场景下的性能提升。 其实在生产环境中,sync.map 用的很少,官方文档推荐的两种使用场景是: > a) when the entry for a given key is only ever written once but read many times, as in caches that only grow. > b) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. 两种场景都比较苛刻,要么是一写多读,要么是各个协程操作的 key 集合没有交集(或者交集很少)。所以官方建议先对自己的场景做性能测评,如果确实能显著提高性能,再使用 sync.map。 > sync.map 的整体思路就是用两个数据结构(只读的 read 和可写的 dirty)尽量将读写操作分开,来减少锁对性能的影响。 - array和slice的区别 > golang array中数组是`值类型`,如果将一个数组赋值到另外一个数组,那么实际上就是整个数组拷贝一份。 - 如果golang中的数组作为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针 - arrary长度也是type的一部分,这样说[10]int和[20]int是不一样的 > slice是一个引用类型,是一个动态的指向数组切片的指针 > slice是一个没有指定固定长度,总是指向底层的数组array的数据结构 区别 > 声明array数组时需要写明数组长度,声明slice时,不需要 > 数组赋值时,array是拷贝赋值,slice是传递指针 - 结构体变量里面,json包变量不加tag的区别 > 都可以正常的转json字符串 区别 > 不加tag,结构体转json字符串,没加tag的字段名跟结构体内字段`原名一致` > 加tag,结构体转json字符串,json字段名就是tag里面的字段名 - 零切片、空切片、nil切片是什么 - nil、空、零切片是切片的三种状态 > nil切片是指在声明时未做初始化的切片,不用分配内存空间,一般用`var` 创建,` var slice []int` > 空切片是`make`创建的空切片需要分配内存空间 ` slice := make([]int,0)` - nil、空切片的长度和容量都是0 > 零切片是指初始值为类型零值的切片 ` slice := make([]int,2,5)` - slice深拷贝和浅拷贝 > 深拷贝:使用内置copy函数来拷贝两个slice > 浅拷贝:指slice变量的赋值操作 ``` func main() { SliceShallowCopy() SliceDeepCopy() } // 浅拷贝 func SliceShallowCopy() { src := []byte {1,2,3,4,5,6} dst := src fmt.Println("before modify[src]:",src) // 输出: before modify[src]: [1 2 3 4 5 6] dst[0]=10 fmt.Println("after modify[src]:",src) // 输出: after modify[src]: [10 2 3 4 5 6] } // 深拷贝 func SliceDeepCopy() { src := []byte {1,2,3,4,5,6} var dst = make([]byte, len(src)) copy(dst[:], src) fmt.Println("before modify[src]:",src) // 输出: before modify[src]: [1 2 3 4 5 6] dst[0]=10 fmt.Println("after modify[src]:",src) // 输出: after modify[src]: [1 2 3 4 5 6] ) ``` - make和new什么区别 - make和new都是用来分配内存的内建函数,且在堆上分配内存 > make是分配内存,也初始化内存,make返回的是引用类型本身。 > new只是将内存清零,并没有初始化内存,new返回的是指向类型的指针。 注意 > new的作用是初始化一个指向类型的指针(*T)。使用new函数来分配空间,传递给new函数的是一个类型,不是一个值。返回的是指向这个新分配的零值的指针。 > make的作用是为slice、map或chan初始化并返回引用(T)。make仅仅用于创建slice、map和channel,并返回它们的实例。 - slice ,map,channel创建的时候的几个参数什么含义 - make函数是go内置函数,它的作用是为`slice`、`map`或`channel`初始化并且返回引用。 > slice: `make([]Type, len, cap)` 创建之后,包含len个类型零值元素。cap可以省略。 > map: `make(map[keyType] valueType, size)` keyType表示map的key类型,valueType表示map的value类型。size是一个整型参数,表示map的存储能力,该参数可省略。 > channel: `make(chan Type, size)` 使用make创建channel,第一个参数是channel类型。size表示缓冲槽大小,是一个可选的大于或等于0的整型参数,默认size = 0。当缓冲槽不为0时,表示通道是一个异步通道。 - slice扩容 - 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍; - 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍; - 如果扩容后的大小仍不能满足,那么新Slice容量等于所需的容量 - 在以上计算完新Slice容量后,交由管理内存的组件申请内存,按照给出的表向上取整进行内存申请,申请出来的内存长度,作为Slice扩容后的容量 - 线程安全的map怎么实现(三种线程安全的map) - 加读写锁 > 常见的map的操作有增删改查和遍历,查和遍历是读操作,增删改是写操作,因此对查和遍历需要加读锁,对增删改需要加写锁。 ``` type RWMap struct { // 一个读写锁保护的线程安全的map sync.RWMutex // 读写锁保护下面的map字段 m map[int]int } // 新建一个RWMap func NewRWMap(n int) *RWMap { return &RWMap{ m: make(map[int]int, n), } } func (m *RWMap) Get(k int) (int, bool) { //从map中读取一个值 m.RLock() defer m.RUnlock() v, existed := m.m[k] // 在锁的保护下从map中读取 return v, existed } func (m *RWMap) Set(k int, v int) { // 设置一个键值对 m.Lock() // 锁保护 defer m.Unlock() m.m[k] = v } func (m *RWMap) Delete(k int) { //删除一个键 m.Lock() // 锁保护 defer m.Unlock() delete(m.m, k) } func (m *RWMap) Len() int { // map的长度 m.RLock() // 锁保护 defer m.RUnlock() return len(m.m) } func (m *RWMap) Each(f func(k, v int) bool) { // 遍历map m.RLock() //遍历期间一直持有读锁 defer m.RUnlock() for k, v := range m.m { if !f(k, v) { return } } } ``` - 分片加锁 > 加锁的对象是整个 map,协程 A 对 map 中的 key 进行修改操作,会导致其它协程无法对其它 key 进行读写操作。一种解决思路是将这个 map 分成 n 块,每个块之间的读写操作都互不干扰,从而降低冲突的可能性。 Go 比较知名的分片 map 的实现是 orcaman/concurrent-map,它的定义如下: ``` var SHARD_COUNT = 32 // 分成SHARD_COUNT个分片的map type ConcurrentMap []*ConcurrentMapShared // 通过RWMutex保护的线程安全的分片,包含一个map type ConcurrentMapShared struct { items map[string]interface{} sync.RWMutex // Read Write mutex, guards access to internal map. } // 创建并发map func New() ConcurrentMap { m := make(ConcurrentMap, SHARD_COUNT) for i := 0; i < SHARD_COUNT; i++ { m[i] = &ConcurrentMapShared{items: make(map[string]interface{})} } return m } // 根据key计算分片索引 func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared { return m[uint(fnv32(key))%uint(SHARD_COUNT)] } ``` ConcurrentMap 其实就是一个切片,切片的每个元素都是第一种方法中携带了读写锁的 map。 GetShard 方法就是用来计算每一个 key 应该分配到哪个分片上 ``` func (m ConcurrentMap) Set(key string, value interface{}) { // 根据key计算出对应的分片 shard := m.GetShard(key) shard.Lock() //对这个分片加锁,执行业务操作 shard.items[key] = value shard.Unlock() } func (m ConcurrentMap) Get(key string) (interface{}, bool) { // 根据key计算出对应的分片 shard := m.GetShard(key) shard.RLock() // 从这个分片读取key的值 val, ok := shard.items[key] shard.RUnlock() return val, ok } ``` Get 和 Set 方法类似,都是根据 key 用 GetShard 计算出分片索引,找到对应的 map 块,执行读写操作。 - sync.map 锁 > sync.map 的整体思路就是用两个数据结构(只读的 read 和可写的 dirty)尽量将读写操作分开,来减少锁对性能的影响。 三种常见的线程安全 map 的实现方式,分别是读写锁、分片锁和 sync.map。较常使用的是前两种,而在特定的场景下,sync.map 的性能会有更优的表现。 - struct能不能比较? - 同一个struct的两个实例可比较也不可比较,当结构不包含不可直接比较成员变量时可直接比较,否则不可直接比较 > 如果结构体中指针类型的值是同一个地址,可以比较, ``` type S struct { Name string Age int Bb *int } av := 1 a := S{ Name: "aa", Age: 1, Bb:&av, } b := S{ Name: "aa", Age: 1, Bb:&av, } fmt.Println(a == b) // 输出: true bv := 2 a = S{ Name: "aa", Age: 1, Bb:&av, } b = S{ Name: "aa", Age: 1, Bb:&bv, } fmt.Println(a == b) // 输出: false ``` - map如何顺序读取 > map用for range遍历不能保证顺序输出,原因:在range时为引用类型(slice,map,channel)创建索引,而map的索引是未被指定的,所以无序。    解决方案:通过sort中的排序包进行对map中的key进行排序。 `sort.String` - go中Set的实现方式 > 对于set类型的数据结构,其实本质上跟list没什么多大的区别,无非是`set不能含有重复的item的特性`,set有初始化、add、clear、remove、contains等操作 > 如何实现基本Set功能,在Java中很容易知道HashSet的底层实现是HashMap,核心的就是用一个常量来填充Map键值对中的Value选项。除此之外,重点关注Go中Map的数据结构,Key是不允许重复的 ``` m := map[string]string{ "1": "one", "2": "two", "1": "one", "3": "three", } fmt.Println(m) // duplicate key "1" in map literal // 程序会直接报错,提示重复Key值,这样就非常符合Set的特性需求了 type Set struct { // struct为结构体类型的变量 m map[interface{}]struct{} } ``` - 使用值为 nil 的 sice、map 会发生啥 > 允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map 添加元素,则会造成运行时 panic。 ``` var m map[string]string m["1"] = "111" fmt.Println(m) // panic: assignment to entry in nil map var m []string m = append(m,"1") fmt.Println(m) // [1] ``` - 访问 map 中的 key,需要注意啥 > 当访问map中不存在的key时,会返回元素对应数据类型的零值,比如nil、""、false和0,区直操作总有值返回,故不能通过取出来的值判断key是否存在 ``` x := map[string]string{"one": "2", "two": "", "three": "3"} fmt.Println(x["aa"]) // 返回空 x := map[string]bool{"one": true, "two":true, "three": true} fmt.Println(x["aa"]) // 返回false // 正确示例 x := map[string]bool{"one": true, "two":true, "three": true} if val,ok := x["aa"];ok { // 判断key是否存在,存在就返回val,则返回如下"不存在" fmt.Println(val) }else{ fmt.Println("不存在") } ``` - string 类型的值可以修改吗 > 不能,尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。 > string 类型的值是只读的二进制 byte slice,如果真要修改字符串中的字符,将 string 转为 []byte 修改后,再转为 string 即可。 ``` // 修改字符串的错误示例 x := "text" x[0] = "T" // error: cannot assign to x[0] fmt.Println(x) // 修改示例 x := "text" xBytes := []byte(x) xBytes[0] = 'T' // 注意此时的 T 是 rune 类型 x = string(xBytes) fmt.Println(x) // Text ``` - 如何关闭 HTTP 的响应体的 > 直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体;手动调用 defer 来关闭响应体。 - 解析 JSON 数据时,默认将数值当做哪种类型 > 在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理。 ``` var data = []byte(`{"status": 200}`) var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { fmt.Println("error:",err) } fmt.Printf("status type:%T\n", result["status"]) // 输出 status type:float64 ``` - 如何从 panic 中恢复 > 在一个 defer 延迟执行的函数中调用 recover ,它便能捕捉/中断 panic ``` // 错误的 recover 调用示例 func main() { recover() // 什么都不会捕捉 panic("not good") // 发生 panic,主程序退出 recover() // 不会被执行 println("ok") } // 正确的 recover 调用示例 func main() { defer func() { fmt.Println("recovered: ", recover()) // 输出:recovered: not good }() panic("not good") } ``` - 说出一个避免Goroutine泄露的措施 > 可以通过 context 包来避免内存泄漏。 > 下面的 for 循环停止取数据时,就用 cancel 函数,让另一个协程停止写数据。如果下面 for 已停止读取数据,上面 for 循环还在写入,就会造成内存泄漏。 ``` func main() { ctx, cancel := context.WithCancel(context.Background()) ch := func(ctx context.Context) <-chan int { ch := make(chan int) go func() { for i := 0; ; i++ { select { case <- ctx.Done(): return case ch <- i: } } } () return ch }(ctx) for v := range ch { fmt.Println(v) if v == 5 { cancel() break } } } ``` - 如何跳出for select 循环 > 通常在for循环中,使用break可以跳出循环,但是注意在go语言中,for select配合时,break 并不能跳出循环。 ``` func testSelectFor2(chExit chan bool){ EXIT: for { select { case v, ok := <-chExit: if !ok { fmt.Println("close channel 2", v) break EXIT//goto EXIT2 } fmt.Println("ch2 val =", v) } } //EXIT2: fmt.Println("exit testSelectFor2") } ``` - Printf()、Sprintf()、Fprintf()函数的区别用法是什么 > 都是把格式好的字符串输出,只是输出的目标不一样。 - Printf(),是把格式字符串输出到标准输出(一般是屏幕,可以重定向)。Printf() 是和标准输出文件 (stdout) 关联的,Fprintf 则没有这个限制。 - Sprintf(),是把格式字符串输出到指定字符串中,所以参数比printf多一个char*。那就是目标字符串地址。 - Fprintf(),是把格式字符串输出到指定文件设备中,所以参数比 printf 多一个文件指针 FILE*。主要用于文件操作。Fprintf() 是格式化输出到一个stream,通常是到文件。 - go语言中的引用类型包含哪些 - 数组切片 - 字典(map) - 通道(channel) - 接口(interface) - go语言中指针运算有哪些 - 可以通过`&`取指针的地址 - 可以通过`*`取指针指向的数据 - go语言的main函数 - main 函数不能带参数 - main 函数不能定义返回值 - main 函数所在的包必须为 main 包 - main 函数中可以使用 flag 包来获取和解析命令行参数 - go语言触发异常的场景有哪些 - 空指针解析 - 下标越界 - 除数为0 - 调用 panic 函数 - go语言的beego框架 - beego 是一个 golang 实现的轻量级HTTP框架 - beego 可以通过注释路由、正则路由等多种方式完成 url 路由注入 - 可以使用 bee new 工具生成空工程,然后使用 bee run 命令自动热编译 - go语言的go-zero框架 > go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验 > go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。 使用 go-zero 的好处: - 轻松获得支撑千万日活服务的稳定性 - 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码 - 微服务治理中间件可无缝集成到其它现有框架使用 - 极简的 API 描述,一键生成各端代码 - 自动校验客户端请求参数合法性 - 大量微服务治理和并发工具包 - go语言的goconvey框架 - goconvey 是一个支持 golang 的单元测试框架 - goconvey 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面 - goconvey 提供了丰富的断言简化测试用例的编写 - GoStub的作用是什么 - GoStub 可以对全局变量打桩 - GoStub 可以对函数打桩 - GoStub 不可以对类的成员方法打桩 - GoStub 可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为 - go语言的select机制 - select 机制用来处理异步 IO 问题 - select 机制最大的一条限制就是每个 case 语句里必须是一个 IO 操作 - golang 在语言级别支持 select 关键字 - nil interface 和 nil interface 的区别 > 虽然 interface 看起来像指针类型,但它不是。interface 类型的变量只有在类型和值均为 nil 时才为 nil如果你的 interface 变量的值是跟随其他变量变化的,与 nil 比较相等时小心。如果你的函数返回值类型是 interface,更要小心这个坑: ``` var data *byte var in interface{} fmt.Println(data, data == nil) // true fmt.Println(in, in == nil) // true in = data fmt.Println(in, in == nil) // false // data 值为 nil,但 in 值不为 nil ``` - select可以用于什么 > 常用语gorotine的完美退出。 > golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作。 ##### 流程控制 - 如果select里只有一个已经关闭的case,会怎么样? > 会死循环 - select 可以用于实现哪些功能 > select 是一个控制结构,类似于switch语句,用于处理异步IO操作。 > select 会监听case语句中channel的读写操作,当case中channel读写作为非阻塞状态(即能读写)时,将会触发相应的动作. - 超时判断 ``` //比如在下面的场景中,使用全局resChan来接受response,如果时间超过3S,resChan中还没有数据返回,则第二条case将执行 var resChan = make(chan int) // do request func test() { select { case data := <-resChan: doData(data) case <-time.After(time.Second * 3): fmt.Println("request time out") } } func doData(data int) { //... } ``` - 判断channel是否阻塞 ``` //在某些情况下是存在不希望channel缓存满了的需求的,可以用如下方法判断 ch := make (chan int, 5) //... data:=0 select { case ch <- data: default: //做相应操作,比如丢弃data。视需求而定 } ``` - context包的用途? > context 包是Go 1.7 引入的标准库,主要用于在goroutine 之间传递`取消信号`、`超时时间`、`截止时间`以及一些`共享的值`等。 它并不是太完美,但几乎成了并发控制和超时控制的标准做法。 使用上,先创建一个根节点的context,之后根据库提供的四个函数创建相应功能的子节点context。 ##### 基础语法 - = 和 := 的区别? - = 是对已经初始化的变量进行赋值(是赋值,在使用前必须var声明) - := 对变量进行初始化赋值(是声明变量并赋值) - go 指针的作用 > 用于操作数据内存,并通过引用修改变量,只声明未赋值的变量,引用类型和指针的零值都为nil,nil类型不能直接赋值,因此需要make初始化数据类型,才能赋值 一个指针可以指向任意变量的地址,它所指向的地址在32位或64位机器上分别固定占4或8个字节。指针的作用有: - 获取变量的值 ``` import fmt ​ func main(){ a := 1 p := &a//取址& fmt.Printf("%d\n", *p);//取值* } ``` - 改变变量的值 ``` // 交换函数 func swap(a, b *int) { *a, *b = *b, *a } ``` - 用指针替代值传入函数,比如类的接收器就是这样的。 ``` type A struct{} ​ func (a *A) fun(){} ``` - Go 允许多个返回值吗? >支持多个返回值,在函数返回值和错误值中,可以返回多个值,比如:test() int,int,error,返回3个类型值 - Go 有异常类型吗? >没有,只有错误类型 error,,可以使用error返回各种错误异常 - 什么是协程(Goroutine) > 协程是与函数同时运行的函数,go协程是轻量级的线程,由go运行来管理,在函数调用前加上go 关键字,这次调用就会在一个新的goroutine 中并发执行。 当被调用的函数返回时,这个goroutine 也自动结束。 > 协程是用户态轻量级线程,它是线程调度的基本单位。通常在函数前加上go关键字就能实现并发。一个Goroutine会以一个很小的栈启动2KB或4KB,当遇到栈空间不足时,栈会自动伸缩, 因此可以轻易实现成千上万个goroutine同时启动。 - 如何高效地拼接字符串 ``` a := "1" b := "2" test := fmt.Sprintf("%s-test-%s",a,b) fmt.Println(test) // 1-test-2 ``` - 如何判断 map 中是否包含某个 key ? ``` var aa map[string]string = map[string]string{ "t1":"t1", "t2":"t2", "t3":"t3", } if _,ok:=aa["t1"];ok { fmt.Println("key已存在") }else{ fmt.Println("key不存在") } // key已存在 ``` - Go 支持默认参数或可选参数吗? > 不支持,但是可以利用结构体参数,或者...传入参数切片数组。 ``` type test struct { num int name string } func sum(data test) { fmt.Println(data.num) } ``` - defer 的执行顺序 >defer执行顺序和调用顺序相反,类似于栈后进先出(LIFO)。`后进先出` ``` defer func(){ fmt.Println(1) } defer func(){ fmt.Println(2) } defer func(){ fmt.Println(3) } // 输出 3 2 1 ``` - 如何交换 2 个变量的值? > 借助临时变量来相会交换值 或者 > 对于变量而言a,b = b,a; 对于指针而言*a,*b = *b, *a - Go 语言 tag 的用处? tag可以为结构体成员提供属性。常见的: - json序列化或反序列化时字段的名称 - db: sqlx模块中对应的数据库字段名 - form: gin框架中对应的前端的数据字段名 - binding: 搭配 form 使用, 默认如果没查找到结构体中的某个字段则不报错值为空, binding为 required 代表没找到返回错误给前端 - 字符串打印时,%v 和 %+v 的区别 - %v 只输出所有的值 - %+v 先输出字段名字,再输出该字段的值 - %#v 先输出结构体名字值,再输出结构体(字段名字+字段的值) - Go 语言中如何表示枚举值(enums)? 在常量中用iota可以表示枚举。iota从0开始。 ``` const ( aa int = iota bb cc dd ) fmt.Println(aa,bb,cc,dd) //输出 0,1,2,3 ``` - ##### 实现原理 - init() 函数是什么时候执行的? - 简答: init()函数会在每个包完成初始化后自动执行,并且执行优先级比main函数高。 - 详细: init()函数是go初始化的一部分,由runtime初始化每个导入的包,初始化不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。 每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的init()函数。同一个包,甚至是同一个源文件可以有多个init()函数。init()函数没有入参和返回值,不能被其他函数调用,同一个包内多个init()函数的执行顺序不作保证。 执行顺序:import –> const –> var –>init()–>main() 一个文件可以有多个init()函数! - Go 语言的局部变量分配在栈上还是堆上? > fmt.println函数使局部变量的作用域超出了函数的作用域,所以局部变量是在堆上. - 2 个 interface 可以比较吗 ``` 可以 1、判断类型是否一样 reflect.TypeOf(a).Kind() == reflect.TypeOf(b).Kind() 2、判断两个interface{}是否相等 reflect.DeepEqual(a, b interface{}) ``` - 2 个 nil 可能不相等吗? > 不相等,2个nil,可能类型不同,接口类型或者变量类型等,比较为false 总结:`两个nil只有在类型相同时才相等。` - Go 语言GC(垃圾回收)的工作原理 > 在垃圾收集时,遍历当前使用的区域,把存活对象复制到另一个区域中,最后将当前使用的区域的可回收对象进行回收。 实现: 首先这个算法会把对分成两块,一块是From、一块是To. 对象只会在From上生成,发生GC之后会找到所有的存活对象,然后将其复制到To区,然后整体回收From区。 - 函数返回局部变量的指针是否安全? > Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。 如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上. > 因为Go会进行逃逸分析,如果发现局部变量的作用域超过该函数则会把指针分配到堆区,避免内存泄漏。 ##### 并发编程 - 无缓冲的 channel 和有缓冲的 channel 的区别? - 无缓冲的 channel: 是同步的,发送与接收同步操作 - 发送的数据如果没有被接收方接收,那么发送方阻塞;如果一直接收不到发送方的数据,接收方阻塞; - 例如make(chan int),就是一个送信人去你家门口送信,你不在家他不走,你一定要接下信,他才会走 - 有缓冲的 channel: 异步, - 发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。可以类比生产者与消费者问题。 - 例如make(chan int, 1),就是一个送信人去你家仍到你家的信箱,转身就走,除非你的信箱满了,他必须等信箱空下来,有缓冲的保证信能进你家的邮箱。 - 什么是协程泄露 > 协程泄露指的是在Go 语言程序中,由于某种原因而导致协程无法正常结束,从而造成内存泄露的情况。 这种情况通常发生在使用协程时处理异步任务时,如果没有正确地处理协程的终止条件,它们将一直保持活动状态,不断占用内存,最终导致内存泄露。 比如:并发协程数量占用的值超过了内存,此时会导致协程无法正常结束,从而造成内存泄露的情况。 协程泄漏是指协程创建之后没有得到释放。主要原因有: - 缺少接收器,导致发送阻塞 - 缺少发送器,导致接收阻塞 - 死锁。多个协程由于竞争资源导致死锁。 - 创建协程的没有回收。 - Go 可以限制运行时操作系统线程的数量吗? > 这是对的,因为Go 对运行时创建的线程数量有一个限制,默认是10000 个线程. #### 进阶 ##### 包管理 - go mod ``` go mod init initialize new module in current directory 在当前目录初始化mod go mod tidy //拉取缺少的模块,移除不用的模块。 go mod download //下载依赖包 go mod vendor //将依赖复制到vendor下 go mod verify //校验依赖 go list -m -json all //依赖详情 go mod graph //打印模块依赖图 go mod why //解释为什么需要依赖 ``` ##### 优化 - golang的内存逃逸吗?什么情况下会发生内存逃逸 > golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在`栈上`分配,否则就说它`逃逸`了,必须在`堆上分配`。 - 能引起变量逃逸到堆上的典型情况: - 在方法内把局部变量指针返回,局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。 - 发送指针或带有指针的值到channel中。在编译时,是没有办法知道哪个goroutine会在channel上接收数据,所以编辑器没法知道变量什么时候被释放。 - 在一个切片上存储指针或者带有指针的值。一个典型就是`[]*string`.这会导致切片的内容逃逸。尽管其后面的数组可能在栈上分配的,但其引用的值一定在堆上。 - slice的背后数组被重新分配了,因为append时可能会超出其容量。slice初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。 - 在interface类型上调用方法。在interface类型上调用方法都是动态调度的, 方法的真正实现只能在运行时知道。想象一个io.Reader类型的变量r,调用r.Read(b)会使得r的值和切片b的背后存储都逃逸掉,所以会在堆上分配。 - 内存泄漏题 [内存泄漏题](https://mp.weixin.qq.com/s/-agtdhlW7Yj7S88a0z7KHg) - Goroutine 泄露的 6 种方法 泄漏的原因大多集中在: - goroutine 内在进行channel/mutex等读写操作,但由于逻辑问题,某种情况下会被一直阻塞。 - goroutine内的业务逻辑进入死循环,资源一直无法释放。 - goroutine内的业务逻辑进入长时间的等待,有不断的新增goroutine进入等待. [泄露](https://segmentfault.com/a/1190000040161853) - go 常见内存泄露的情况 - 获取长字符串中的一段导致长字符串未释放 - 获取长slice中一段导致长slice未释放 - 在长slice新建slice导致泄露 - goroutine泄露 - time.Ticker未关闭导致泄露 - Finalizer导致泄露 - Deferring Function Call导致泄漏 - sync.Pool [文档](https://juejin.cn/post/6989798306440282148) > sync.Pool是一个并发安全的缓存池,能够并发且安全地存储、获取元素/对象。常用于对象实例创建会占用较多资源的场景 > sync.Pool是以缓存池的形式,在创建对象等场景下,减少GC次数、提高程序稳定性。 1、创建一个 Pool 实例 Pool 池的模式是通用型的(存储对象的类型为interface{}),所有的类型的对象都可以进行使用。注意的是,作为使用方不能对 Pool 里面的对象个数做假定,同时也无法获取 Pool 池中对象个数。 ``` pool := &sync.Pool{} ``` 2、Put 和 Get 接口 (1) Put(interface{}):将一个对象加入到 Pool 池中 - 注意的是这仅仅是把对象放入池子,池子中的对象真正释放的时机是不受外部控制的。 (2) Get() interface{}:返回 Pool 池中存在的对象 - 注意的是 Get() 方法是随机取出对象,无法保证以固定的顺序获取 Pool 池中存储的对象。 3、为 Pool 实例配置 New 方法 没有配置 New 方法时,如果 Get 操作多于 Put 操作,继续 Get 会得到一个 nil interface{} 对象,所以需要代码进行兼容。 配置 New 方法后,Get 获取不到对象时(Pool 池中已经没有对象了),会调用自定义的 New 方法创建一个对象并返回。 ``` pool := &sync.Pool { New: func() interface {} { return struct{}{} } } ``` > 注意的是,sync.Pool 本身数据结构是并发安全的,但是 Pool.New 函数(用户自定义的)不一定是线程安全的。并且 Pool.New 函数可能会被并发调用,如果 New 函数里面的实现逻辑是非并发安全的,那就会有问题。 4、关于 sync.Pool 的性能优势,可以试一下 go/src/sync/pool_test.go 中的几个压测函数。 - 使用场景 1、增加临时对象的重用率(sync.Pool 本质用途)。在高并发业务场景下出现 GC 问题时,可以使用 sync.Pool 减少 GC 负担。 2、不适合存储带状态的对象,因为获取对象是随机的(Get 到的对象可能是刚创建的,也可能是之前创建并 cache 住的),并且缓存对象的释放策略完全由 runtime 内部管理。像 socket 长连接、数据库连接,有人说适合有人说不适合。 3、不适合需要控制缓存元素个数的场景,因为无法知道 Pool 池里面的对象个数,以及对象的释放时机也是未知的。 4、典型的应用场景之一是在网络包收取发送的时候,用 sync.Pool 会有奇效,可以大大降低 GC 压力。 - 实践 1、使用规范:作为对象生成器 (a) 初始化 sync.Pool 实例,并需要配置并发安全的 New 方法; (b) 创建对象的地方,通过 Pool.Get() 获取; (c) 在 Get 后马上进行 defer Pool.Put(x); 2、使用建议:不要对 Pool 池中的对象做任何假定 即使用前需要清理缓存对象,有两种方案: 在调用 Pool.Put 前进行 memset 对象操作; 在 Pool.Get 操作后对对象进行 memset 操作 ##### 并发编程 - 对已经关闭的的chan进行读写,会怎么样? - 读已经关闭的`chan`能一直读到东西,但是读到的内容根据通道内`关闭前`是否有元素而不同 - 如果`chan`关闭前,`buffer`内有元素还未读,会正确读到`chan`内的值,且返回的第二个bool值(是否读取成功)为true. - 如果`chan`关闭前,`buffer`内有元素已经被读取完,`chan`内无值,接下来所有接收的值都会非阻塞直接成功,返回`channel`元素的零值,但是第二个`bool`值一直是false. - 写已经关闭的`chan`会panic 1.写已经关闭的 chan ``` func main() { c := make(chan int,3) close(c) c <- 123 fmt.Println(<-c) // panic: send on closed channel } ``` 2.读已经关闭的chan ``` func main() { c := make(chan int,3) c <- 123 close(c) val,ok := <-c fmt.Println(val,ok) // 123,true val1,ok1 := <-c fmt.Println(val1,ok1) // 0,false } ``` - 对未初始化的的chan进行读写,会怎么样 > 读写未初始化的chan都会阻塞。 > 未初始化的chan此时是等于 nil ,当它不能阻塞的情况下,直接返回 false ,表示写(读) chan 失败 ``` func main() { var c chan int c <- 123 fmt.Println(<-c) //fatal error: all goroutines are asleep - deadlock! //goroutine 1 [chan send (nil chan)]: } ``` - sync.map 的优缺点和使用场景 [文档](https://studygolang.com/articles/22128) - 主协程如何等其余协程完再操作 - channel 实现同步 ``` package main import ( "fmt" ) func printString(str string) { for _, data := range str { fmt.Printf("----%c", data) } fmt.Printf("\n") } var ch = make(chan int) var tongBu = make(chan int) func person1() { fmt.Println("person1") printString("Gerald") tongBu <- 1 ch <- 1 } func person2() { fmt.Println("person2") <- tongBu printString("Seligman") ch <- 2 } func main() { // 目的:使用 channel 来实现 person1 先于 person2 执行 go person1() go person2() count := 2 // 判断所有协程是否退出 for range ch { count-- if 0 == count { close(ch) } } } // 输出 // person2 // person1 // ----G----e----r----a----l----d // ----S----e----l----i----g----m----a----n count 表示有所少个协程 ch 用来子协程与主协程之间的同步 tongBu 用来两个协程之间的同步 主协程阻塞等待数据,每当一个子协程执行完后,就会往 ch 里面写一个数据,主协程收到后会使 count–,当 count 减为 0,关闭 ch,主协程将不阻塞在 range ch。 ``` - sync.WaitGroup Go 语言提供一个更简单的方式就是,sync.WaitGroup 来实现等待。 sync.WaitGroup 内部是实现了一个计数器,它有三个方法 - Add() 用来设置一个计数 - Done() 用来在操作结束时调用,使计数减1 - Wait() 用来等待所有的操作结束,即计数变为0。 ``` package main import ( "fmt" "sync" ) func printString(str string) { for _, data := range str { fmt.Printf("%c", data) } fmt.Printf("\n") } // 使用 sync.WaitGroup 的方式来实现主协程等待其他子协程 var wg sync.WaitGroup var tongBu = make(chan int) func person1() { printString("Gerald") tongBu <- 1 wg.Done() } func person2() { <- tongBu printString("Seligman") wg.Done() } func main() { wg.Add(2) // 目的:使用 channel 来实现 person1 先于 person2 执行 go person1() go person2() defer close(tongBu) wg.Wait() } ``` - 有缓存的channel和没有缓存的channel区别是什么 - 通道又叫做`channel`,channel作用就是在多线程之间传递数据的 - channel是一种特殊的类型,在任何时候,同事只能有一个goroutine访问通道进行发送和获取数据 - 通道像一个传送带或者队列,总是遵循`先进先出`的规则,保证收发数据的顺序 - channel分为有缓存和无缓存两种。 1、无缓存 ``` ch := make(chan int) ch <- 1 go func() { <-ch fmt.Println("1") }() fmt.Println("2") // fatal error: all goroutines are asleep - deadlock! ``` 错误原因: - 创建一个无缓冲的channel,然后给channel赋值,程序就在赋值后陷入了死锁。 - 因为channel是无缓存,即同步的,赋值完之后不及时读取channel,程序就会阻塞 - channel的机制是`先进先出`,如果给channel赋值了,那么必须要读取它的值,不然会造成阻塞,当然对于这种无缓存的channel有效。对于`有缓存的channel`,发送方会一直阻塞直到数据被拷贝到缓冲区;如果缓冲区已经满,则发送方只能在接收方取走数据才能从阻塞状态恢复。 2.有缓存 ``` ch := make(chan int,1) ch <- 1 go func() { <-ch fmt.Println("1") }() time.Sleep(1 * time.Second) fmt.Println("2") // 输出: 1 2 ``` 总结 ``` c1:=make(chan int) 无缓冲 // 无缓冲的 不仅仅是 向 c1 通道放 1 而是 一直要有别的携程 <-c1 接手了 这个参数,那么c1<-1才会继续下去,要不然就一直阻塞着 c2:=make(chan int,1) 有缓冲 // 而 c2<-1 则不会阻塞,因为缓冲大小是1 只有当 放第二个值的时候 第一个还没被人拿走,这时候才会阻塞。 ``` - channel 无缓存,即同步,写读同步操作,不然阻塞 - channel 有缓存,即异步,带缓冲的channel可以让发送在缓冲范围内不阻塞线程,但是channel的接收还是会照常被阻塞的。 - 协程通信方式有哪些? > 在go中协程间通信的方式有多种,最常用的channel。如果牵扯多个协程的通知,可以使用sync.Cond。 1.进程间通信,常用方式: - 有名管道 - 无名管道 - 信号 - 共享内存 - 消息队列 - 信号灯集 - socket 2.线程间通信,常用方式: - 信号量 - 互斥锁 - 条件变量 > 协程间通信方式,官方推荐使用channel,channel在一对一的协程之间进行数据交换与通信十分便捷。但是,一对多的广播场景中,则显得有点无力,此时就需要sync.Cond来辅助。 ##### 高级特性 - 能说说uintptr和unsafe.Pointer的区别吗? - unsafe.Pointer只是单纯的`通用指针类型`,用于转换不同类型指针,它不可以参与指针运算 - uintptr是用于指针运算,GC不把uintptr当指针,也就是说uintptr无法持有对象,uintptr类型的目标会被回收 - unsafe.Pointer可以和普通的指针进行类型转换 - unsafe.Pointer可以和uintptr进行相互转换 - 协程和线程的区别 - 线程 > 线程是指进程内一个执行单元,也是进程内的可调度实体。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的基本单位。 > 线程自己基本不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计算器,一组寄存器和栈),但是它可与同属于一个进程的其他线程共享进程所拥有的全部资源。 > 线程之间通信主要通过共享内存,上下文切换快,资源开销少,但相比进程不够稳定,容易丢失数据。 > 进程是操作系统的一部分,负责执行应用程序。在系统上执行的每个程序都是一个进程,并且要在应用程序内部运行代码,进程使用称为线程的术语。线程是轻量级进程,或者换句话说,线程是执行程序下代码的单元。所以每个程序都有逻辑,一个线程负责执行这个逻辑。 - 协程 > 协程是一种用户态的轻量级线程,协程的调度完全是由用户控制,从技术角度来说`协程是你可以暂停的函数`。协程拥有自己的寄存器上下文和栈。 > 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来时候,恢复先前保存的寄存器和上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的全局变量,所以上下文切换很快。 > 协程是一个函数或方法,它与程序中存在的任何其他 协程 一起独立并同时执行。或者换句话说,Go 语言中每个并发执行的活动都称为协程(Goroutines)。 - 协程和线程区别 - 一个线程可以有多个协程,一个进程也可以单独拥有多个协程。 - 线程进程都是同步机制,而协程是异步。 - 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。 - 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。 - 协程并不是取代线程,而且抽象于线程之上。线程是被分割的 CPU 资源,协程是组织好的代码流程,协程需要线程来承载运行,线程是协程的资源,但协程不会直接使用线程,协程直接利用的是执行器(Interceptor),执行器可以关联任意线程或线程池,可以使当前线程,UI线程或新建新程 - 线程是协程的资源。协程通过 Interceptor 来间接使用线程这个资源。 - 上下文指的是什么 ? 举个例子 : > 当某个线程占用CPU时间过长时, 操作系统的调度器就会强制下线依此来保证每个线程在一段时间内运行的时间差别不大的. 那么此时进行调度, 就需要发生上下文的切换, 因为我们下次再运行这个线程时, 需要记录上一次运行的各种条件. 例如记录上一次的重要寄存器值, 进程状态等等. 这些都存储在线程控制块中(TCB). > 上下文更为浅显的意思就是 : 所需要依赖的环境, 这次的线程退出我们需要记录其重要的值和信息以便下次再上CPU时, 可以从上一次的末尾开始, 就不必重新开始执行. 新上来的线程, 也需要加载上一次执行的各种数据以便这次执行更方便 - GPM模型 [文档](https://zboya.github.io/post/go_scheduler/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io) ##### 图解网络基础 - 漫画图解HTTP知识点 [文档](https://mp.weixin.qq.com/s/wNRoDoW_VEqiq8JelePj2g) ##### 数据库 - 数据库三大范式是什么 - 第一范式:确保每列保持原子性 (如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式) - 第二范式:确保表中的每列都和主键相关 (一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中,子表关联,减小了数据库的冗余) - 第三范式:确保每列都和主键列直接相关,而不是间接相关 注意:大部分按照功能需求来设计 - mysql有关权限的表都有哪几个 > MySQL 服务器通过权限表来控制用户对数据库的访问,权限表存放在 MySQL 数据库里,由 mysql_install_db 脚本初始化,这些权限表分别 user,db,table_priv,columns_priv 和 host。 - *user 权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的; - *db 权限表:记录各个帐号在各个数据库上的操作权限; - *table_priv 权限表:记录数据表级的操作权限; - *columns_priv 权限表:记录数据列级的操作权限; - *host 权限表:配合 db 权限表对给定主机上数据库级操作权限作更细致的控制;这个权限表不受 GRANT 和 REVOKE 语句的影响 - MySQL的binlog有有几种录入格式?分别有什么区别? > 有三种格式,statement,row和mixed。 - statement:statement模式下,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。 - row:row级别下,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。 - mixed:一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。 此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录。 - mysql有哪些数据类型 > mysql支持多种类型,大致分为三类:数值、日期/时间、字符串(字符)类型 [文档](https://www.runoob.com/mysql/mysql-data-types.html) - MySQL存储引擎MyISAM与InnoDB区别 - myisam 类型不支持事务处理等高级处理,MYISAM类型的表强调的是性能,执行速度比InnoDB类型更快 - innoDB 类型支持事务处理等高级处理,以及外部键等高级数据库功能。 两种存储引擎的区别: - InnoDB支持事务,MyISAM不支持 - InnoDB支持外键,MyISAM不支持 - InnoDB不支持fulltext类型的索引 - 清空整个表数据时,InnoDB是一行一行删除,效率很慢,MyISAM则会重建表 - InnoDB支持行锁(某些情况下还是锁整表,如 update table set a=1 where user like '%lee%' - MyISAM索引与InnoDB索引的区别 - InnoDB 索引是聚簇索引,MyISAM 索引是非聚簇索引 - InnoDB 的主键索引的叶子节点存储着行数据,主键索引非常高效 - MyISAM 索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据 - InnoDB 非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效 - InnoDB引擎的4大特性 - 插入缓冲(Insert Buffer/Change Buffer) - 双写机制(Double Write) - 自适应哈希索引(Adaptive Hash Index,AHI) - 预读(Read Ahead) [文档](https://blog.csdn.net/weixin_45320660/article/details/115326483) - 存储引擎选择 MySQL存储引擎特性汇总和对比 | 特性 | MyISAM | InnoDB | MEMORY | | ---- | ---- | ---- | ---- | | 存储限制 | 有 | 支持 | 有 | | 事务安全 | 不支持 | 支持 | 不支持 | | 锁机制 | 表锁 | 行锁 | 表锁 | | B树索引 | 支持 | 支持 | 支持 | | 哈希索引 | 不支持 | 不支持 | 支持 | | 全文索引 | 支持 | 不支持 | 不支持 | | 集群索引 | 不支持 | 支持 | 不支持 | | 数据缓存 | | 支持 | 支持 | | 索引缓存 | 支持 | 支持 | 支持 | | 数据可压缩 | 支持 | 不支持 | 不支持 | | 空间使用 | 低 | 高 | N/A | | 内存使用 | 低 | 高 | 中等 | | 批量插入速度 | 高 | 低 | 高 | | 支持外键 | 不支持 | 支持 | 不支持 | - 什么是索引 - 索引是一种特殊的数据库结构,由数据表中的一列或多列组合而成,可以用来快速查询数据表中某一特定值的记录。 - 通过索引,查询数据时不用扫全表读取所有信息记录,而只是查询索引列。否则,数据库系统将对每条记录的所有信息进行匹配。 - 索引可以很大程度上提高数据库查询的速度,还有效的提高了数据库系统的性能。 1、没有索引的访问(顺序访问) > 顺序访问是在表中实行全表扫描,从头到尾逐行遍历,直到在无序的行数据中找到符合条件的目标数据。 2、索引访问 > 索引访问是通过遍历索引来直接访问表中记录行的方式。 3、索引的优缺点 索引的优点如下: - 通过创建唯一索引可以保证数据库表中每一行数据的唯一性。 - 可以给所有的mysql列类型设置索引 - 可以大大加快数据的查询速度 缺点: - 创建和维护索引组要耗费时间,并且随着数据量的增加所耗费的时间也会增加。 - 索引需要占磁盘空间,除了数据表占数据空间以外,每一个索引还要占一定的物理空间。如果有大量的索引,索引文件可能比数据文件更快达到最大文件尺寸。 - 当对表中的数据进行增加、删除和修改的时候,索引也要动态维护,这样就降低了数据的维护速度。 总结:索引可以提高查询速度,但是会影响插入记录的速度。因为,向有索引的表中插入记录时,数据库系统会按照索引进行排序,这样就降低了插入记录的速度,插入大量记录时的速度影响会更加明显。这种情况下,最好的办法是先删除表中的索引,然后插入数据,插入完成后,再创建索引。 - 索引有哪几种类型 mysql 常见的索引 - 唯一索引:在创建唯一索引时要不能给具有相同的索引值 - 主键索引:在我们给一个字段设置主键的时候,它就会自动创建主键索引,用来确保每一个值都是唯一的。 - 普通索引:它的结构主要以B+树和哈希索引为主,主要是对数据表中的数据进行精确查找。 - 全文索引:它的作用是搜索数据表中的字段是不是包含我们搜索的关键字,就像搜索引擎中的模糊查询。 - 聚集索引:我们在表中添加数据的顺序,与我们创建的索引键值相同,而且一个表中只能有一个聚集索引。 - 创建索引的原则 1. 选择唯一性索引 > 唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录. 例如,学生表中学号是具有唯一性的字段。为该字段建立唯一性索引可以很快的确定某个学生的信息。如果使用姓名的话,可能存在同名现象,从而降低查询速度。 2. 为经常需要排序、分组和联合操作的字段建立索引 > 经常需要ORDER BY、GROUP BY、DISTINCT和UNION等操作的字段,排序操作会浪费很多时间。如果为其建立索引,可以有效地避免排序操作。 3. 为常作为查询条件的字段建立索引 > 如果某个字段经常用来做查询条件,那么该字段的查询速度会影响整个表的查询速度。因此,为这样的字段建立索引,可以提高整个表的查询速度。 4. 限制索引的数目 > 索引的数目不是越多越好。每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。 5. 尽量使用数据量少的索引 > 如果索引的值很长,那么查询的速度会受到影响。例如,对一个CHAR(100)类型的字段进行全文检索需要的时间肯定要比对CHAR(10)类型的字段需要的时间要多。 6. 尽量使用前缀来索引 > 如果索引字段的值很长,最好使用值的前缀来索引。例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间。如果只检索字段的前面的若干个字符,这样可以提高检索速度。 7. 删除不再使用或者很少使用的索引 > 表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。 `注意:选择索引的最终目的是为了使查询的速度变快。` - 创建索引时需要注意什么 1. 非空字段:应该制定列为非null,因为null值很难优化。 如果要存储null值,则应该用0之类的代替。 2. 取值离散大的字段:离散大的字段放到联合索引的前面 3. 索引字段越小越好:数据库存储以页为单位,字段越小,一次io得到的数据越多。 - 使用索引查询一定能提高查询的性能吗?为什么 > 使用索引查询不一定能提高查询的性能,因为索引的建立和使用都有一定的消耗。 如果数据量较少,查询速度不必太慢,此时索引可能会增加存储空间的消耗,但并不能提高查询的性能。 如果数据量很大,查询速度很慢,此时索引可以通过加快查询速度来提高查询的性能。 通常,通过索引查询数据比全表扫描要快.但是我们也必须注意到它的代价. > 索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改. 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O. 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢.使用索引查询不一定能提高查询性能. 索引范围查询(INDEX RANGE SCAN)适用于两种情况: - 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%宜采用; - 基于非唯一性索引的检索 - 数据库删除大批量千万级百万级数据的优化 - 删除之前,做个完整备份。 - 删除前先保存当前索引的DDL,然后删除其索引, - 根据使用的删除条件建立一个临时的索引(这是提高速度的另外一个重要原因!) - 开始删除操作,完成之后再重建之前的索引。 - mysql 前缀索引 > 当要索引的列字符很多时 索引则会很大且变慢 ( 可以只索引列开始的部分字符串 节约索引空间 从而提高索引效率 ) `原则: 降低重复的索引值` 添加前缀索引 ( 以第N位字符创建前缀索引 ) > alter table x_test add index(x_name(N)) - 联合索引是什么?为什么需要注意联合索引中的顺序? 对多个字段同时建立的索引(有顺序,ABC,ACB是完全不同的两种联合索引。) 使用时注意什么 - 单个索引需要注意的事项,组合索引全部通用。比如索引列不要参与计算啊、or的两侧要么都索引列,要么都不是索引列啊、模糊匹配的时候%不要在头部啦等等 - 最左匹配原则。(A,B,C) 这样3列,mysql会首先匹配A,然后再B,C.如果用(B,C)这样的数据来检索的话,就会找不到A使得索引失效。如果使用(A,C)这样的数据来检索的话,就会先找到所有A的值然后匹配C,此时联合索引是失效的。 - 把最常用的,筛选数据最多的字段放在左侧。 - 数据库事务 - Atomicity(原子性):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。 - Consistency(一致性):数据库总是从一个一致性状态转换到另一个一致状态。 - Isolation(隔离性):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。 - Durability(持久性):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。(持久性的安全性与刷新日志级别也存在一定关系,不同的级别对应不同的数据安全级别。) 并发事务带来的问题 - 更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 --最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一 文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。 最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同 一文件,则可避免此问题。 - 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前, 这条记录的数据就处于不一致状态; 这时, 另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。 - 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读” 。 - 幻读 (Phantom Reads): 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读” 。 幻读和不可重复读的区别: - 不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(因为中间有其他事务提交了修改) - 幻读的重点在于新增或者删除:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(因为中间有其他事务提交了插入/删除) 并发事务处理带来的问题的解决办法: - “更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。 - “脏读” 、 “不可重复读”和“幻读” ,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决: - 一种是加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。 - 另一种是数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。 - 按照锁的粒度分数据库锁有哪些?锁机制与InnoDB锁算法? 在关系型数据库中,可以按照锁的粒度把数据库锁分为: - 行级锁(INNODB引擎) - 表级锁(MYISAM引擎) - 页级锁(BDB引擎 ) `InnoDB 支持表级锁和行级锁,MyISAM 只支持表级锁` 行级锁,表级锁和页级锁对比: - 行级锁:MySQL中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁。 - 特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 - 表级锁:MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。 - 特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。 - 页级锁:是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。 - 特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般 - MySQL中InnoDB引擎的行锁是怎么实现的? > MySQL InnoDB 行锁是通过给索引上的索引项加锁来实现的。 Oracle 是通过在数据块中对相应数据行加锁来实现的。 > MySQL InnoDB这种行锁实现特点意味着: 只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁 - 什么是死锁?怎么解决? 1、设置超时机制 2、死锁检测工具,发现超时,主动停止其中事务处理,让其他事务正常处理。 - 什么是存储过程?有哪些优缺点? 存储过程(Stored Procedure)是一组为了完成特定功能的SQL 语句集,经编译后存储在数据库。用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。 优点: - 重复使用。存储过程可以重复使用,从而可以减少数据库开发人员的工作量。 - 减少网络流量。存储过程位于服务器上,调用的时候只需要传递存储过程的名称以及参数就可以了,因此降低了网络传输的数据量。 - 安全性。参数化的存储过程可以防止SQL注入式攻击,而且可以将Grant、Deny以及Revoke权限应用于存储过程。 缺点: - 调试麻烦 - 开发和维护成本很高 - 什么是触发器?触发器的使用场景有哪些? [触发器介绍](https://blog.csdn.net/weixin_42333061/article/details/108805224) - MySQL中都有哪些触发器? > MySQL 的触发器和存储过程一样,都是嵌入到 MySQL 中的一段程序,是存储在数据库目录中的一组SQL语句集合,是 MySQL 中管理数据的有力工具。 `在 MySQL 中,只有执行 INSERT、UPDATE 和 DELETE 操作时才能激活触发器,其它 SQL 语句则不会激活触发器。` MySQL触发器有三种:(在 INSERT/UPDATE/DELETE 语句执行之前或之后响应的触发器。) - INSERT 触发器 - UPDATE 触发器 - DELETE 触发器 - MySQL常用基本SQL语句 [常用基本SQL语句](https://www.51cto.com/article/692339.html) - SQL语句主要分为哪几类 SQL语言共分为四大类: - 数据查询语言DQL (由SELECT子句,FROM子句,WHERE子句组成的查询块) - 数据操纵语言DML (INSERT、UPDATE、DELETE) - 数据定义语言DDL (CREATE TABLE、VIEW、INDEX、SYN、CLUSTER) - 数据控制语言DCL (GRANT:授权、事务) - 超键、候选键、主键、外键分别是什么? - 超键(super key):在关系中能唯一标识元组的属性集称为关系模式的超键 - 候选键(candidate key):不含有多余属性的超键称为候选键 - 主键(primary key):用户选作元组标识的一个候选键程序主键 - 外键(foreign key)如果关系模式R1中的某属性集不是R1的主键,而是另一个关系R2的主键则该属性集是关系模式R1的外键。 假设有如下两个表: - 学生(学号,姓名,性别,身份证号,教师编号) - 教师(教师编号,姓名,工资) 超键:由超键的定义可知,学生表中含有学号或者身份证号的任意组合都为此表的超键。如:(学号)、(学号,姓名)、(身份证号,性别)等。(组合唯一键) 候选键:属于超键,它是最小的超键,就是说如果再去掉候选键中的任何一个属性它就不再是超键了。学生表中的候选键为:(学号)、(身份证号)。 主键:就是候选键里面的一个,是人为规定的,例如学生表中,我们通常会让“学号”做主键,教师表中让“教师编号”做主键。(唯一主键) 外键:学生表中的外键就是“教师编号”。外键主要是用来描述两个表的关系。 - SQL 约束有哪几种? `在MySQL里,“约束”指的是对表中数据的一种限制约束,它能够确保数据库中数据的准确性和有效性。` 1、NOT NULL (非空约束): 强制字段列不接受NULL空值 2、UNIQUE(唯一约束):约束唯一标识数据库表中的每条记录。 3、PRIMARY KEY(主键约束) - 约束唯一标识数据库表中的每条记录。 - 任意两行的主键值都不相同。 - 主键必须包含唯一的值。 - 主键列不能为空。 - 每个表都应该有个主键,但只能有一个主键。 - 主键值不能重复。如果从表中删除某一行,其主键值不分配新行。 `主键值的列从不修改或更新。(大多数DBMS不允许这样做,但是如果你使用的DBMS允许这样做,那也,千万别!!!)` 4、FOREIGN KEY(外键约束):外键是表中的一列,其值必须列在另一个表的主键中,也就是说一个表中的 FOREIGN KEY 指向另一个表中的 PRIMARY KEY。 5、CHECK (检查约束) 检查约束用来保证一列(或一组列)中的数据满足一组指定的条件。 CHECK约束用于限制列中的值的范围。 只允许特定的值。例如,在性别字典中只允许M或F。 如果一个表定义CHECK约束,那么此约束会在特定的列对值进行限制。 6、DEFAULT(默认约束) DEFAULT 约束用于向列中插入默认值。 每个字段只能有一个默认约束。 如果默认约束设置的值大于字段所允许的长度,则截取到字段允许长度。 如果没有规定其他的值,那么会将默认值添加到所有的新记录。 - 六种关联查询 [关联查询](https://juejin.cn/post/6844904137025404936) - varchar与char的区别 - char:定长,效率高,一般用于固定长度的表单提交数据存储;例如:身份证号,手机号,电话,密码等。 - varchar:不定长,效率偏低。 > 长度的区别,char范围是0~255,varchar最长是64k,但是注意这里的64k是整个row的长度,要考虑到其它的 column,还有如果存在not null的时候也会占用一位,对不同的字符集,有效长度还不一样,比如utf8的,最多21845,还要除去别的column,但是varchar在一般 情况下存储都够用了。如果遇到了大文本,考虑使用text,最大能到4G。 效率来说基本是char>varchar>text,但是如果使用的是Innodb引擎的话,推荐使用varchar代替char。 char和varchar可以有默认值,text不能指定默认值。 - varchar(50)中50的涵义 最多占用50个字符(表示存储数据的大小) - int(20)中20的涵义 int(M)只是用来显示数据的宽度,我们能看到的宽度(表示数据的宽度) int(10)的意思是假设有一个变量名为id,它的能显示的宽度能显示10位。在使用id时,假如我给id输入10,那么mysql会默认给你存储0000000010。当你输入的数据不足10位时,会自动帮你补全位数。假如我设计的id字段是int(20),那么我在给id输入10时,mysql会自动补全18个0,补到20位为止。 - drop、delete与truncate的区别 - drop:删除表,并释放空间 - delete:删除表里面指定的数据/删除整个表里面的所有数据 - DELETE FROM test WHERE id = 1; 删除数据 - DELETE FROM test; 删除整个表里面的所有数据 - truncate:删除表里面的数据,并释放空间,但不删除表,表结构还在。 - UNION与UNION ALL的区别? - union:将多个结果合并在一起显示出来,不包括重复行,同时进行默认规则的排序; - union all: 将多个结果合并在一起显示出来,包括重复行,不进行排序; union和union all的区别是 union会自动压缩多个结果集合中的重复结果, union all则将所有的结果全部显示出来,不管是不是重复。 - 如何定位及优化SQL语句的性能问题? 查看使用索引的使用情况 explain - 大表数据查询,怎么优化? 1、建立索引,因为索引可以很大程度优化查询 2、可以配置缓存还可以用slow_query_log进行分析,这样很大提升查询的 3、建立分库分表,因为分库分表是查询的杀手锏 4、优化sql语句,比如子查询的优化 ``` 实践出真知。根据成本顺序依次是: 第一:加索引优化sql。尽量避免全盘扫描,另单表索引也不是越多越好。 第二:加缓存。使用redis,memcached,但注意缓存同步更新、设置失效等问题。 第三:主从复制,读写分离。适合读多写少的场景,同步会有延迟。 第四:垂直拆分。可以选用适当的中间件Mycat等 第五:水平切分。选择合理的sharding key,改动表结构,将大数据字段拆分出去,对经常查询的字段做一定的冗余,同时做好数据同步。 ``` - MySql处理超大分页方法和原理 [超大分页](https://blog.csdn.net/thetimelyrain/article/details/110954818) - 慢查询日志解析 [慢查询](https://zhuanlan.zhihu.com/p/112307303) - 为什么要尽量设定一个主键? 主键是数据库确保数据行在整张表唯一性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID列作为主键. 设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全. - 主键使用自增ID还是UUID? ``` 自增ID与UUID的比较: 1、自增ID是有序的,而UUID是随机的。前面已经说了,如果主键是有序的,数据库可以具有更好的性能(至少对MySQL而已是如此) 2、自增ID所需的存储空间比UUID要小 3、由于自增ID比UUID更加简单,因此生成自增ID的生成速度也比UUID更快 4、自增ID与数据相关,主键会暴露出去的话,自增ID会显示当前表中的数据规模;而UUID则无此风险 5、自增ID在不同的数据库中可能重复,在分布式的环境下无法保证唯一。而UUID在分布式环境下也可以保证唯一 自增ID在性能上更有优势,而UUID则更加适应分布式场景 如果数据量非常大需要分库,或者需要更好的安全性,那么使用UUID 对于非敏感数据或者数据量没有大需要分库,使用自增id能节省存储空间并获得更好的性能 ``` - MySQL数据库cpu飙升到500%的话他怎么处理 [数据库cpu飙升](https://blog.51cto.com/lxw1844912514/2938096)