注:该博客仅为个人学习笔记。
学习笔记 go-zero框架
switch
构造的一种形式。我们在切换后指定一个变量。chan
关键字用于定义通道。在执行
中,允许您同时运行并行代码。const
关键字用于标量值引入名称,常量continue
使用关键字可以返回到for
循环的开头,跳过当前循环default
语句是可选的,在switch
语句中使用case和default.如果值与表达式不匹配,则跳到默认值。defer
用于推迟执行功能,直到周围的功能执行为止,如果是在函数中最后执行if
条件为假,则执行else下的语句switch
语句中使用该关键字。当我们使用该关键字时,将执行下面的case条件for
开始for循环func
关键字声明一个函数go
关键字触发一个goroutine(异步处理),该例程由golang运行时管理goto
关键字可无条件跳转至带标签的语句if
语句用于检查循环内的特定条件。import
关键字用于导入软件包。interface
关键字用于指定方法集。方法集时一种类型的方法列表。map
关键字定义map类型。映射是键值对的无序集合。package
关键字代码在包中分组为一个单元。类似代码文件在文件夹中的统一包名。range
关键字可以迭代列表(map或者数组)。遍历循环(map或者数组)。select
关键字使goroutine在同步通信操作期间等待处理。struct
是字段的集合。我们可以在字段声明后使用struct关键字,定义结构体。switch
语句用于启动循环并在块内使用if-else逻辑。type
我们可以使用type
关键字引入新的结构类型。var
关键字用于创建go语言的变量标识符是指go语言对各种函数、方法、变量等命名时使用的字符序列,标识符由若干个字母、下划线
_
、和数字组成,并且第一个字符必须是字母。 下划线_
是一个特殊的标识符,称之为空白标识符,它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),它赋值给它的值都将被抛弃,因此不可以使用_
作为变量给其他变量进行赋值或运算。 变量、类型、函数或者代码内标识符的名称不能重复。
_
组成break
、if
等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之间通过通道就可以通信。
通道像一个传送带或者队列,总是遵循先进先出
的规则,保证收发数据的顺序。
特点:
把数据往通道中发送时,如果接收方一直没有接收,那么发送操作将持续阻塞。
如果接收方接收时,通道中没有发送方发送的数据,接收方也会发送阻塞,直到发送方发送数据为止
通道一次只能接收一个元素。
函数类型
可以把函数作为一种变量,用
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及所有完全定义了==与!=操作的类型。
var map变量名 map[key] value
关键字: 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对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。
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 遍历的返回值有一定的规律:
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)。
三种类型的函数:
普通函数声明(定义)
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
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 能非常方便地处理资源释放问题。
使用延迟并发解锁
函数中并发使用 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 的组合有如下特性:
Test功能测试函数
完善的测试体系,能够提高开发的效率,当项目足够复杂的时候,想要保证尽可能的减少 bug,有两种有效的方式分别是代码审核和测试,Go语言中提供了 testing 包来实现单元测试功能。 要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀,例如:
func TestXxx( t *testing.T ){
//......
}
编写测试用例有以下几点需要注意:
_test.go
结尾(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 的类型(一般为集合),均可使用该包中的方法进行排序。这些方法要求集合内列出元素的索引为整数。
使用上述函数就用判断该数组是否已经排序
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中数组是
值类型
,如果将一个数组赋值到另外一个数组,那么实际上就是整个数组拷贝一份。
slice是一个引用类型,是一个动态的指向数组切片的指针 slice是一个没有指定固定长度,总是指向底层的数组array的数据结构
区别
声明array数组时需要写明数组长度,声明slice时,不需要 数组赋值时,array是拷贝赋值,slice是传递指针
结构体变量里面,json包变量不加tag的区别
都可以正常的转json字符串 区别 不加tag,结构体转json字符串,没加tag的字段名跟结构体内字段
原名一致
加tag,结构体转json字符串,json字段名就是tag里面的字段名
零切片、空切片、nil切片是什么
nil切片是指在声明时未做初始化的切片,不用分配内存空间,一般用
var
创建,var slice []int
空切片是make
创建的空切片需要分配内存空间slice := make([]int,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是分配内存,也初始化内存,make返回的是引用类型本身。 new只是将内存清零,并没有初始化内存,new返回的是指向类型的指针。 注意 new的作用是初始化一个指向类型的指针(*T)。使用new函数来分配空间,传递给new函数的是一个类型,不是一个值。返回的是指向这个新分配的零值的指针。 make的作用是为slice、map或chan初始化并返回引用(T)。make仅仅用于创建slice、map和channel,并返回它们的实例。
slice ,map,channel创建的时候的几个参数什么含义
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扩容
线程安全的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 的整体思路就是用两个数据结构(只读的 read 和可写的 dirty)尽量将读写操作分开,来减少锁对性能的影响。
三种常见的线程安全 map 的实现方式,分别是读写锁、分片锁和 sync.map。较常使用的是前两种,而在特定的场景下,sync.map 的性能会有更优的表现。
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()函数的区别用法是什么
都是把格式好的字符串输出,只是输出的目标不一样。
go语言中的引用类型包含哪些
go语言中指针运算有哪些
&
取指针的地址*
取指针指向的数据go语言的main函数
go语言触发异常的场景有哪些
go语言的beego框架
go语言的go-zero框架
go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验 go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
使用 go-zero 的好处:
go语言的goconvey框架
GoStub的作用是什么
go语言的select机制
nil interface 和 nil interface 的区别
虽然 interface 看起来像指针类型,但它不是。interface 类型的变量只有在类型和值均为 nil 时才为 nil如果你的 interface 变量的值是跟随其他变量变化的,与 nil 比较相等时小心。如果你的函数返回值类型是 interface,更要小心这个坑:
var data *byte
var in interface{}
fmt.Println(data, data == nil) // <nil> true
fmt.Println(in, in == nil) // <nil> true
in = data
fmt.Println(in, in == nil) // <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缓存满了的需求的,可以用如下方法判断
ch := make (chan int, 5)
//...
data:=0
select {
case ch <- data:
default:
//做相应操作,比如丢弃data。视需求而定
}
context包的用途?
context 包是Go 1.7 引入的标准库,主要用于在goroutine 之间传递
取消信号
、超时时间
、截止时间
以及一些共享的值
等。 它并不是太完美,但几乎成了并发控制和超时控制的标准做法。 使用上,先创建一个根节点的context,之后根据库提供的四个函数创建相应功能的子节点context。
= 和 := 的区别?
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可以为结构体成员提供属性。常见的:
字符串打印时,%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 的区别?
什么是协程泄露
协程泄露指的是在Go 语言程序中,由于某种原因而导致协程无法正常结束,从而造成内存泄露的情况。 这种情况通常发生在使用协程时处理异步任务时,如果没有正确地处理协程的终止条件,它们将一直保持活动状态,不断占用内存,最终导致内存泄露。
比如:并发协程数量占用的值超过了内存,此时会导致协程无法正常结束,从而造成内存泄露的情况。
协程泄漏是指协程创建之后没有得到释放。主要原因有:
Go 可以限制运行时操作系统线程的数量吗?
这是对的,因为Go 对运行时创建的线程数量有一个限制,默认是10000 个线程.
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程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在
栈上
分配,否则就说它逃逸
了,必须在堆上分配
。
[]*string
.这会导致切片的内容逃逸。尽管其后面的数组可能在栈上分配的,但其引用的值一定在堆上。内存泄漏题 内存泄漏题
Goroutine 泄露的 6 种方法 泄漏的原因大多集中在:
go 常见内存泄露的情况
sync.Pool 文档
sync.Pool是一个并发安全的缓存池,能够并发且安全地存储、获取元素/对象。常用于对象实例创建会占用较多资源的场景 sync.Pool是以缓存池的形式,在创建对象等场景下,减少GC次数、提高程序稳定性。 1、创建一个 Pool 实例 Pool 池的模式是通用型的(存储对象的类型为interface{}),所有的类型的对象都可以进行使用。注意的是,作为使用方不能对 Pool 里面的对象个数做假定,同时也无法获取 Pool 池中对象个数。
pool := &sync.Pool{}
2、Put 和 Get 接口 (1) Put(interface{}):将一个对象加入到 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 中的几个压测函数。
对已经关闭的的chan进行读写,会怎么样?
chan
能一直读到东西,但是读到的内容根据通道内关闭前
是否有元素而不同
chan
关闭前,buffer
内有元素还未读,会正确读到chan
内的值,且返回的第二个bool值(是否读取成功)为true.chan
关闭前,buffer
内有元素已经被读取完,chan
内无值,接下来所有接收的值都会非阻塞直接成功,返回channel
元素的零值,但是第二个bool
值一直是false.chan
会panic1.写已经关闭的 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 的优缺点和使用场景 文档
主协程如何等其余协程完再操作
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。
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作用就是在多线程之间传递数据的先进先出
的规则,保证收发数据的顺序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
,发送方会一直阻塞直到数据被拷贝到缓冲区;如果缓冲区已经满,则发送方只能在接收方取走数据才能从阻塞状态恢复。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 只有当 放第二个值的时候 第一个还没被人拿走,这时候才会阻塞。
协程通信方式有哪些?
在go中协程间通信的方式有多种,最常用的channel。如果牵扯多个协程的通知,可以使用sync.Cond。
1.进程间通信,常用方式:
2.线程间通信,常用方式:
协程间通信方式,官方推荐使用channel,channel在一对一的协程之间进行数据交换与通信十分便捷。但是,一对多的广播场景中,则显得有点无力,此时就需要sync.Cond来辅助。
能说说uintptr和unsafe.Pointer的区别吗?
通用指针类型
,用于转换不同类型指针,它不可以参与指针运算协程和线程的区别
线程
线程是指进程内一个执行单元,也是进程内的可调度实体。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的基本单位。 线程自己基本不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计算器,一组寄存器和栈),但是它可与同属于一个进程的其他线程共享进程所拥有的全部资源。 线程之间通信主要通过共享内存,上下文切换快,资源开销少,但相比进程不够稳定,容易丢失数据。 进程是操作系统的一部分,负责执行应用程序。在系统上执行的每个程序都是一个进程,并且要在应用程序内部运行代码,进程使用称为线程的术语。线程是轻量级进程,或者换句话说,线程是执行程序下代码的单元。所以每个程序都有逻辑,一个线程负责执行这个逻辑。
协程
协程是一种用户态的轻量级线程,协程的调度完全是由用户控制,从技术角度来说
协程是你可以暂停的函数
。协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来时候,恢复先前保存的寄存器和上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的全局变量,所以上下文切换很快。 协程是一个函数或方法,它与程序中存在的任何其他 协程 一起独立并同时执行。或者换句话说,Go 语言中每个并发执行的活动都称为协程(Goroutines)。
协程和线程区别
上下文指的是什么 ? 举个例子 :
当某个线程占用CPU时间过长时, 操作系统的调度器就会强制下线依此来保证每个线程在一段时间内运行的时间差别不大的. 那么此时进行调度, 就需要发生上下文的切换, 因为我们下次再运行这个线程时, 需要记录上一次运行的各种条件. 例如记录上一次的重要寄存器值, 进程状态等等. 这些都存储在线程控制块中(TCB). 上下文更为浅显的意思就是 : 所需要依赖的环境, 这次的线程退出我们需要记录其重要的值和信息以便下次再上CPU时, 可以从上一次的末尾开始, 就不必重新开始执行. 新上来的线程, 也需要加载上一次执行的各种数据以便这次执行更方便
GPM模型 文档
数据库三大范式是什么
注意:大部分按照功能需求来设计
mysql有关权限的表都有哪几个
MySQL 服务器通过权限表来控制用户对数据库的访问,权限表存放在 MySQL 数据库里,由 mysql_install_db 脚本初始化,这些权限表分别 user,db,table_priv,columns_priv 和 host。
MySQL的binlog有有几种录入格式?分别有什么区别?
有三种格式,statement,row和mixed。
mysql有哪些数据类型
mysql支持多种类型,大致分为三类:数值、日期/时间、字符串(字符)类型 文档
MySQL存储引擎MyISAM与InnoDB区别
两种存储引擎的区别:
MyISAM索引与InnoDB索引的区别
InnoDB引擎的4大特性
存储引擎选择 MySQL存储引擎特性汇总和对比
特性 | MyISAM | InnoDB | MEMORY |
---|---|---|---|
存储限制 | 有 | 支持 | 有 |
事务安全 | 不支持 | 支持 | 不支持 |
锁机制 | 表锁 | 行锁 | 表锁 |
B树索引 | 支持 | 支持 | 支持 |
哈希索引 | 不支持 | 不支持 | 支持 |
全文索引 | 支持 | 不支持 | 不支持 |
集群索引 | 不支持 | 支持 | 不支持 |
数据缓存 | 支持 | 支持 | |
索引缓存 | 支持 | 支持 | 支持 |
数据可压缩 | 支持 | 不支持 | 不支持 |
空间使用 | 低 | 高 | N/A |
内存使用 | 低 | 高 | 中等 |
批量插入速度 | 高 | 低 | 高 |
支持外键 | 不支持 | 支持 | 不支持 |
什么是索引
1、没有索引的访问(顺序访问)
顺序访问是在表中实行全表扫描,从头到尾逐行遍历,直到在无序的行数据中找到符合条件的目标数据。
2、索引访问
索引访问是通过遍历索引来直接访问表中记录行的方式。
3、索引的优缺点 索引的优点如下:
缺点:
总结:索引可以提高查询速度,但是会影响插入记录的速度。因为,向有索引的表中插入记录时,数据库系统会按照索引进行排序,这样就降低了插入记录的速度,插入大量记录时的速度影响会更加明显。这种情况下,最好的办法是先删除表中的索引,然后插入数据,插入完成后,再创建索引。
索引有哪几种类型 mysql 常见的索引
创建索引的原则
唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录. 例如,学生表中学号是具有唯一性的字段。为该字段建立唯一性索引可以很快的确定某个学生的信息。如果使用姓名的话,可能存在同名现象,从而降低查询速度。
经常需要ORDER BY、GROUP BY、DISTINCT和UNION等操作的字段,排序操作会浪费很多时间。如果为其建立索引,可以有效地避免排序操作。
如果某个字段经常用来做查询条件,那么该字段的查询速度会影响整个表的查询速度。因此,为这样的字段建立索引,可以提高整个表的查询速度。
索引的数目不是越多越好。每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。
如果索引的值很长,那么查询的速度会受到影响。例如,对一个CHAR(100)类型的字段进行全文检索需要的时间肯定要比对CHAR(10)类型的字段需要的时间要多。
如果索引字段的值很长,最好使用值的前缀来索引。例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间。如果只检索字段的前面的若干个字符,这样可以提高检索速度。
表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。
注意:选择索引的最终目的是为了使查询的速度变快。
创建索引时需要注意什么
使用索引查询一定能提高查询的性能吗?为什么
使用索引查询不一定能提高查询的性能,因为索引的建立和使用都有一定的消耗。 如果数据量较少,查询速度不必太慢,此时索引可能会增加存储空间的消耗,但并不能提高查询的性能。 如果数据量很大,查询速度很慢,此时索引可以通过加快查询速度来提高查询的性能。
通常,通过索引查询数据比全表扫描要快.但是我们也必须注意到它的代价.
索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改. 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O. 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢.使用索引查询不一定能提高查询性能.
索引范围查询(INDEX RANGE SCAN)适用于两种情况:
数据库删除大批量千万级百万级数据的优化
mysql 前缀索引
当要索引的列字符很多时 索引则会很大且变慢 ( 可以只索引列开始的部分字符串 节约索引空间 从而提高索引效率 )
原则: 降低重复的索引值
添加前缀索引 ( 以第N位字符创建前缀索引 ) alter table x_test add index(x_name(N))
联合索引是什么?为什么需要注意联合索引中的顺序? 对多个字段同时建立的索引(有顺序,ABC,ACB是完全不同的两种联合索引。)
使用时注意什么
数据库事务
并发事务带来的问题
幻读和不可重复读的区别:
并发事务处理带来的问题的解决办法:
按照锁的粒度分数据库锁有哪些?锁机制与InnoDB锁算法? 在关系型数据库中,可以按照锁的粒度把数据库锁分为:
InnoDB 支持表级锁和行级锁,MyISAM 只支持表级锁
行级锁,表级锁和页级锁对比:
MySQL中InnoDB引擎的行锁是怎么实现的?
MySQL InnoDB 行锁是通过给索引上的索引项加锁来实现的。 Oracle 是通过在数据块中对相应数据行加锁来实现的。 MySQL InnoDB这种行锁实现特点意味着: 只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁
什么是死锁?怎么解决? 1、设置超时机制 2、死锁检测工具,发现超时,主动停止其中事务处理,让其他事务正常处理。
什么是存储过程?有哪些优缺点? 存储过程(Stored Procedure)是一组为了完成特定功能的SQL 语句集,经编译后存储在数据库。用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。 优点:
缺点:
什么是触发器?触发器的使用场景有哪些?
MySQL中都有哪些触发器?
MySQL 的触发器和存储过程一样,都是嵌入到 MySQL 中的一段程序,是存储在数据库目录中的一组SQL语句集合,是 MySQL 中管理数据的有力工具。
在 MySQL 中,只有执行 INSERT、UPDATE 和 DELETE 操作时才能激活触发器,其它 SQL 语句则不会激活触发器。
MySQL触发器有三种:(在 INSERT/UPDATE/DELETE 语句执行之前或之后响应的触发器。)
MySQL常用基本SQL语句 常用基本SQL语句
SQL语句主要分为哪几类 SQL语言共分为四大类:
超键、候选键、主键、外键分别是什么?
假设有如下两个表:
超键:由超键的定义可知,学生表中含有学号或者身份证号的任意组合都为此表的超键。如:(学号)、(学号,姓名)、(身份证号,性别)等。(组合唯一键)
候选键:属于超键,它是最小的超键,就是说如果再去掉候选键中的任何一个属性它就不再是超键了。学生表中的候选键为:(学号)、(身份证号)。
主键:就是候选键里面的一个,是人为规定的,例如学生表中,我们通常会让“学号”做主键,教师表中让“教师编号”做主键。(唯一主键)
外键:学生表中的外键就是“教师编号”。外键主要是用来描述两个表的关系。
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 约束用于向列中插入默认值。
每个字段只能有一个默认约束。
如果默认约束设置的值大于字段所允许的长度,则截取到字段允许长度。
如果没有规定其他的值,那么会将默认值添加到所有的新记录。
六种关联查询
varchar与char的区别
长度的区别,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的区别
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处理超大分页方法和原理
慢查询日志解析
为什么要尽量设定一个主键?
主键是数据库确保数据行在整张表唯一性的保障,即使业务上本张表没有主键,也建议添加一个自增长的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%的话他怎么处理
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。