go基础类
1. go优势
* 天生支持并发,性能高
* 单一的标准代码格式,比其它语言更具可读性
* 自动垃圾收集比java和python更有效,因为它与程序同时执行
go数据类型
int string float bool array slice map channel pointer struct interface method
go程序中的包是什么
* 项目中包含go源文件以及其它包的目录,源文件中的函数、变量、类型都存储在该包中
* 每个源文件都属于一个包,该包在文件顶部使用 package packageName 声明
* 我们在源文件中需要导入第三方包时需要使用 import packageName
go支持什么形式的类型转换?将整数转换为浮点数
* go支持显示类型转换,以满足严格的类型要
* a := 15
* b := float64(a)
* fmt.Println(b, reflect.TypeOf(b))
什么是 goroutine,你如何停止它?
* goroutine是协程/轻量级线程/用户态线程,不同于传统的内核态线程
* 占用资源特别少,创建和销毁只在用户态执行不会到内核态,节省时间
* 创建goroutine需要使用go关键字
* 可以向goroutine发送一个信号通道来停止它,goroutine内部需要检查信号通道
例子:
```
func main() {
var wg sync.WaitGroup
var exit = make(chan bool)
wg.Add(1)
go func() {
for {
select {
case <-exit: // 接收到信号后return退出当前goroutine
fmt.Println("goroutine接收到信号退出了!")
wg.Done()
return
default:
fmt.Println("还没有接收到信号")
}
}
}()
exit <- true
wg.Wait()
}
```
如何在运行时检查变量类型
* 类型开关(Type Switch)是在运行时检查变量类型的最佳方式。
* 类型开关按类型而不是值来评估变量。每个 Switch 至少包含一个 case 用作条件语句
* 如果没有一个 case 为真,则执行 default。
go两个接口之间可以存在什么关系
* 如果两个接口有相同的方法列表,那么他俩就是等价的,可以相互赋值
* 接口A可以嵌套到接口B里面,那么接口B就有了自己的方法列表+接口A的方法列表
go中同步锁(互斥锁)有什么特点,作用是什么?何时使用互斥锁,何时使用读写锁?
* 当一个goroutine获得了Mutex后,其它goroutine就只能乖乖等待,除非该goroutine释放Mutex
* RWMutext在读锁占用的情况下会阻止写,但不会阻止读,在写锁占用的情况下,会阻止任何其它goroutine进来
* 无论是读还是写,整个锁相当于由该goroutine独占
* 作用:保证资源在使用时的独有性,不会因为并发导致数据错乱,保证系统稳定性
* 案例:
```
package main
import (
"fmt"
"sync"
"time"
)
var (
num = 0
lock = sync.RWMutex{} // 耗时:100+毫秒
//lock = sync.Mutex{} // 耗时:50+毫秒
)
func main() {
start := time.Now()
go func() {
for i := 0; i < 100000; i++{
lock.Lock()
//fmt.Println(num)
num++
lock.Unlock()
}
}()
for i := 0; i < 100000; i++{
lock.Lock()
//fmt.Println(num)
num++
lock.Unlock()
}
fmt.Println(num)
fmt.Println(time.Now().Sub(start))
}
```
// 结论:
// 1. 如果对数据写的比较多,使用Mutex同步锁/互斥锁性能更高
// 2. 如果对数据读的比较多,使用RWMutex读写锁性能更高
goroutine案例(两个goroutine,一个负责输出数字,另一个负责输出26个英文字母,格式如下:12ab34cd56ef78gh ... yz)
package main
import (
"fmt"
"sync"
"unicode/utf8"
)
// 案例:两个goroutine,一个负责输出数字,另一个负责输出26个英文字母,格式如下:12ab34cd56ef78gh ... yz
var (
wg = sync.WaitGroup{}
chNum = make(chan bool)
chAlpha = make(chan bool)
)
func main() {
go func() {
i := 1
for {
<-chNum
fmt.Printf("%v%v", i, i + 1)
i += 2
chAlpha <- true
}
}()
wg.Add(1)
go func() {
str := "abcdefghigklmnopqrstuvwxyz"
i := 0
for {
<-chAlpha
fmt.Printf("%v", str[i:i+2])
i += 2
if i >= utf8.RuneCountInString(str){
wg.Done()
return
}
chNum <- true
}
}()
chNum <- true
wg.Wait()
}
go语言中,channel通道有什么特点,需要注意什么?
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var ch chan int
var ch1 = make(chan int)
fmt.Println(ch, ch1) // <nil> 0xc000086060
wg.Add(1)
go func() {
//ch <- 15 // 如果给一个nil的channel发送数据会造成永久阻塞
//<-ch // 如果从一个nil的channel中接收数据也会造成永久阻塞
ret := <-ch1
fmt.Println(ret)
ret = <-ch1 // 从一个已关闭的通道中接收数据,如果缓冲区中为空,则返回该类型的零值
fmt.Println(ret)
wg.Done()
}()
go func() {
//close(ch1)
ch1 <- 15 // 给一个已关闭通道发送数据就会包panic错误
close(ch1)
}()
wg.Wait()
}
- 结论:
- 给一个nil channel发送数据时会一直堵塞
- 从一个nil channel接收数据时会一直阻塞
- 给一个已关闭的channel发送数据时会panic
- 从一个已关闭的channel中读取数据时,如果channel为空,则返回通道中类型的领零值
go中channel缓冲有什么特点?
go中的cap函数可以作用于哪些内容?
- 可作用于的类型有
- 数组(array)
- 切片(slice)
- 通道(channel)
go convey是什么,一般用来做什么?
- go convey是一个支持golang的单元测试框架
- 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面
- 提供了丰富的断言简化测试用例的编写
go语言中new的作用是什么?
- 使用new函数来分配内存空间
- 传递给new函数的是一个类型,而不是一个值
- 返回值是指向这个新分配的地址的指针
go语言中的make作用是什么?
- 分配内存空间并进行初始化, 返回值是该类型的实例而不是指针
- make只能接收三种类型当做参数:slice、map、channel
总结new和make的区别?
- new可以接收任意内置类型当做参数,返回的是对应类型的指针
- make只能接收slice、map、channel当做参数,返回值是对应类型的实例
Printf、Sprintf、FprintF都是格式化输出,有什么不同?
- 虽然这三个函数都是格式化输出,但是输出的目标不一样
- Printf输出到控制台
- Sprintf结果赋值给返回值
- FprintF输出到指定的io.Writer接口中
例如:
func main() {
var a int = 15
file, _ := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND, 0644)
// 格式化字符串并输出到文件
n, _ := fmt.Fprintf(file, "%T:%v:%p", a, a, &a)
fmt.Println(n)
}
go语言中的数组和切片的区别是什么?
- 数组:
- 数组固定长度,数组长度是数组类型的一部分,所以[3]int和[4]int是两种不同的数组类型
- 数组类型需要指定大小,不指定也会根据初始化,自动推算出大小,大小不可改变,数组是通过值传递的
- 切片:
- 切片的长度可改变,切片是轻量级的数据结构,三个属性:指针、长度、容量
- 不要指定切片的大小,切片也是值传递只不过切片的一个属性指针指向的数据不变,所以看起来像引用传递
- 切片可以通过数组来初始化也可以通过make函数来初始化,初始化时的len和cap相等,然后进行扩容
- 切片扩容的时候会导致底层的数组复制,也就是切片中的指针属性会发生变化
- 切片也是拷贝,在不发生扩容时,底层使用的是同一个数组,当对其中一个切片append的时候, 该切片长度会增加
但是不会影响另外一个切片的长度
- copy函数将原切片拷贝到目标切片,会导致底层数组复制,因为目标切片需要通过make函数来声明初始化内存,然后
将原切片指向的数组元素拷贝到新切片指向的数组元素
- 重点:数组保存真正的数据,切片值保存数组的指针和该切片的长度和容量
- append函数如果切片容量足够的话,只会影响当前切片的长度,数组底层不会复制,不会影响与数组关联的其它切片的长度
- copy直接会导致数组底层复制
go语言中值传递和地址传递(引用传递)如何运行?有什么区别?举例说明
- 值传递会把参数的值复制一份放到对应的函数里,两个变量的地址不同,不可互相修改
- 地址传递会把参数的地址复制一份放到对应的函数里,两个变量的地址相同,可以互相修改
- 例如:数组传递就是值传递,而切片传递就是数组的地址传递(本质上切片值传递,只不过是保存的数据地址相同)
go中数组和切片在传递时有什么区别?
- 数组是值传递
- 切片地址传递(引用传递)
go中是如何实现切片扩容的?
- 当容量小于1024时,每次扩容容量翻倍,当容量大于1024时,每次扩容加25%
func main() {
s1 := make([]int, 0)
for i := 0; i < 3000; i++{
fmt.Println("len =", len(s1), "cap = ", cap(s1))
s1 = append(s1, i)
}
}
看下面代码defer的执行顺序是什么?defer的作用和特点是什么?
- 在普通函数或方法前加上defer关键字,就完成了defer所需要的语法,当defer语句被执行时,跟在defer语句后的函数会被延迟执行
- 知道包含该defer语句的函数执行完毕,defer语句后的函数才会执行,无论包含defer语句的函数是通过return正常结束,还是通过panic导致的异常结束
- 可以在一个函数中执行多条defer语句,由于在栈中存储,所以它的执行顺序和声明顺序相反
defer语句中通过recover捕获panic例子
func main() {
defer func() {
err := recover()
fmt.Println(err)
}()
defer fmt.Println("first defer")
defer fmt.Println("second defer")
defer fmt.Println("third defer")
fmt.Println("哈哈哈哈")
panic("abc is an error")
}
go中的25个关键字
- 程序声明2
package import
- 程序实体声明和定义8
var const type func struct map chan interface
- 程序流程控制15
for range continue break select switch case default if else fallthrough defer go goto return
写一个定时任务,每秒执行一次
func main() {
t1 := time.NewTicker(time.Second * 1)
var i = 1
for {
if i == 10{
break
}
select {
case <-t1.C: // 一秒执行一次的定时任务
task1(i)
i++
}
}
}
func task1(i int) {
fmt.Println("task1执行了---", i)
}
switch case fallthrough default使用场景
func main() {
var a int
for i := 0; i < 10; i++{
a = rand.Intn(100)
switch {
case a >= 80:
fmt.Println("优秀", a)
fallthrough
case a >= 60:
fmt.Println("及格", a)
fallthrough
default:
fmt.Println("不及格", a)
}
}
}
defer的常用场景
- defer语句经常被用于处理成对的操作打开/关闭,链接/断开连接,加锁/释放锁
- 通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放
- 释放资源的defer语句应该直接跟在请求资源处理错误之后
- 注意:defer一定要放在请求资源处理错误之后
go中slice的底层实现
- 切片是基于数组实现的,它的底层是数组,它本身非常小,它可以理解为对底层数组的抽闲
- 因为基于数组实现,所以它的底层内存是连续分配的,效率非常高,还可以通过索引获取数据
- 切片本身并不是动态数组或数组指针,它内部实现的数据结构体通过指针引用底层数组
- 设定相关属性将读写操作限定在指定的区域内,切片本身是一个只读对象,其工作机制类似于数组指针的一种封装
- 切片对象非常小,因为它只有三个字段的数据结构:指向底层数组的指针、切片的长度、切片的容量
go中slice的扩容机制,有什么注意点?
- 首先判断,如果新申请的容量大于2倍的旧容量,最终容量就是新申请的容量
- 否则判断,如果旧切片的长度小于1024,最终容量就是旧容量的两倍
- 否则判断,如果旧切片的长度大于等于1024,则最终容量从旧容量开始循环增加原来的1/4,直到最终容量大于新申请的容量
- 如果最终容量计算值溢出,则最终容量就是新申请的容量
扩容前后的slice是否相同?
- 情况一:
- 原来数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容之后的切片还是指向原来的数组
- 对一个切片的操作可能影响多个指针指向相同地址的切片
- 情况二:
- 原来数组的容量已经达到了最大值,在扩容,go默认会先开辟一块内存区域,把原来的值拷贝过来
- 然后再执行append操作,这种情况丝毫不影响原数组
- 注意:要复制一个slice最好使用copy函数
go中的参数传递、引用传递
- go语言中的所有的传参都是值传递(传值),都是一个副本,一个拷贝,
- 因为拷贝的内容有时候是非引用类型(int, string, struct)等,这样在函数中就无法修改原内容数据
- 有的是引用类型(指针、slice、map、chan),这样就可以修改原内容数据
- go中的引用类型包含slice、map、chan,它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性
- 内置函数new计算类型大小,为其分配零值内存,返回指针。
- 而make会被编译器翻译成具体的创建函数,由其分配内存并初始化成员结构,返回对象而非指针
哈希概念讲解
- 哈希表又称为散列表,由一个直接寻址表和一个哈希函数组成
- 由于哈希表的大小是有限的而要存储的数值是无限的,因此对于任何哈希函数,
- 都会出现两个不同元素映射到相同位置的情况,这种情况叫做哈希冲突
- 通过拉链法解决哈希冲突:
* 哈希表每个位置都连接一个链表,当冲突发生是,冲突的元素将会被加到该位置链表的最后
- 哈希表的查找速度起决定性作用的就是哈希函数: 除法哈希发、乘法哈希法、全域哈希法
- 哈希表的应用?
- 字典与集合都是通过哈希表来实现的
- md5曾经是密码学中常用的哈希函数,可以吧任意长度的数据映射为128位的哈希值
go中的map底层实现
- go中map的底层实现就是一个散列表,因此实现map的过程实际上就是实现散列表的过程
- 在这个散列表中,主要出现的结构体由两个,一个是hmap、一个是bmap
- go中也有一个哈希函数,用来对map中的键生成哈希值
- hash结果的低位用于把k/v放到bmap数组中的哪个bmap中
- 高位用于key的快速预览,快速试错
go中的map如何扩容
- 翻倍扩容:如果map中的键值对个数/桶的个数>6.5,就会引发翻倍扩容
- 等量扩容:当B<=15时,如果溢出桶的个数>=2的B次方就会引发等量扩容
- 当B>15时,如果溢出桶的个数>=2的15次方时就会引发等量扩容
go中map的查找
- go中的map采用的是哈希查找表,由哈希函数通过key和哈希因此计算出哈希值,
- 根据hamp中的B来确定放到哪个桶中,如果B=5,那么就根据哈希值的后5位确定放到哪个桶中
- 在用哈希值的高8位确定桶中的位置,如果当前的bmap中未找到,则去对应的overflow bucket中查找
- 如果当前map处于数据搬迁状态,则优先从oldbuckets中查找
介绍一下channel
- go中不要通过共享内存来通信,而要通过通信实现共享内存
- go中的csp并发模型,中文名通信顺序进程,就是通过goroutine和channel实现的
- channel收发遵循先进先出,分为有缓冲通道(异步通道),无缓冲通道(同步通道)
go中channel的特性
- 给一个nil的channel发送数据,会造成永久阻塞
- 从一个nil的channel接收数据,会造成永久阻塞
- 给一个已经关闭的channel发送数据,会造成panic
- 从一个已经关闭的channel接收数据,如果缓冲区为空,会返回零值
- 无缓冲的channel是同步的,有缓冲的channel是异步的
- 关闭一个nil channel会造成panic
channel中ring buffer的实现
- channel中使用了ring buffer(环形缓冲区)来缓存写入数据,
- ring buffer有很多好处,而且非常适合实现FiFo的固定长度队列
- channel中包含buffer、sendx、recvx
- recvx指向最早被读取的位置,sendx指向再次写入时插入的位置
请发表评论