GO的常见小问题
byte与int
byte与字符串
strings转换
for循环字符串
简短模式
1 | var size := 1024 |
这种是不对的,会报错
常量内存分配
1 | package main |
会报错
常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用
goto
1 | func main() { |
goto不能跳转到其他函数或者内层代码
新类型强转
1 | func main() { |
会报错,myint的是新类型,得强转,不能直接赋值
第二个,user取了别名myuser1,myuser1没有m2方法,执行失败
nil切片和空切片指向地址
指向数组的引用地址不一样
nil切片是0,不存在
空切片是指向一个固定的地址,所有的空切片都指向这个地址
var slice []int 创建出来的 slice 其实是一个 nil slice。它的长度和容量都为0。和nil比较的结果为true。这里比较混淆的是empty slice,empty slice的长度和容量也都为0,但是所有的空切片的数据指针都指向同一个地址 0xc42003bda0。空切片和 nil 比较的结果为false
字符串转换数组,内存拷贝问题
会发生内存拷贝,只要发生类型强转,都会发生内存拷贝
go的unsafe包
1 | unsafe.Pointer(&a)方法可以得到变量a的地址 |
反转含有中文,数字,字符串的字符
1 | package main |
rune关键字,从golang源码中看出,它是int32的别名(-2^31 ~ 2^31-1),比起byte(-128~127),可表示更多的字符。
由于rune可表示的范围更大,所以能处理一切字符,当然也包括中文字符。在平时计算中文字符,可用rune。
因此将字符串转为rune的切片,再进行翻转,完美解决
拷贝大切片,小切片的代价
并不是,所有切片的大小相同;三个字段(一个uintptr,两个int)。切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个 slice 变量分配给另一个变量只会复制三个机器字。所以 拷贝大切片跟小切片的代价应该是一样的
大切片跟小切片的区别无非就是 Len 和 Cap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段
uintptr和unsafe.Pointer的区别
unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;
而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收;
unsafe.Pointer 可以和 普通指针 进行相互转换;
unsafe.Pointer 可以和 uintptr 进行相互转换
1 | package main |
uintptr(unsafe.Pointer(w)) 获取了 w 的指针起始值
unsafe.Offsetof(w.b) 获取 b 变量的偏移量
两个相加就得到了 b 的地址值,将通用指针 Pointer 转换成具体指针 ((*int)(b)),通过 * 符号取值,然后赋值。*((*int)(b)) 相当于把 (*int)(b) 转换成 int 了,最后对变量重新赋值成 10,这样指针运算就完成了
map不初始化会怎么样
map不初始化,可以取值,取出来的值是空
不能赋值,赋值会panic异常
空map和nil map结果是一样的,都是map[], 判断是否为空, map==nil
delete一个nil map也不会panic
map初始化和不初始化的区别
初始化map是有长度的,可以赋值,操作,默认值。不初始化无法赋值
map的遍历删除
map的range遍历删除是安全的,可以一边遍历一边删除,但是删除完后,占用的内存一直不释放,map = nil释放内存
go的字符串修改
字符串是不能修改的,天生线程安全,大家使用的都是只读对象,无须加锁;再者,方便内存共享,而不必使用写时复制(Copy On Write)等技术;字符串 hash 值也只需要制作一份
怎么修改,转为byte数组进行替换,最后通过string强转
1 | angleBytes := []byte(angel) |
判断一个数组是否已经排序
sort包的,isSorted方法
sort包
1 | type Interface interface { |
slice和map的并发安全处理
slice在并发执行中不会报错,但是数据会丢失
方法:1,索引处理,全局索引,2:加锁, 3:channel处理
map并发会报错
方法:1,加锁,2:sync.map
数组与切片的区别
1、切片是指针类型, 数组是值类型
2、数组的长度是固定的,切片不是,切片是动态数组
3、切片比数组多一个属性,cap容量
4、切片的底层是数组
json包使用,结构体里tag问题
如果变量首字母小写,则为private。无论如何不能转,因为取不到反射信息
如果变量首字母大写,则为public。
不加tag,可以正常转为json里的字段,json内字段名跟结构体内字段原名一致。
加了tag,从struct转json的时候,json的字段名就是tag里的字段名,原字段名已经没用。
slice的深拷贝和 浅拷贝
浅拷贝对于值类型的话是完全拷贝一份,而对于引用类型是拷贝其地址。也就是拷贝的对象修改引用类型的变量同样会影响到源对象
P2 := P1就是浅拷贝,如何避免,重新给定地址,重新生成一个
深拷贝 deepCopy(dst, src interface{})
对于深拷贝就比较好了解了,任何对象都会被完完整整的拷贝一份,拷贝对象与被拷贝对象不存在如何联系,也就不会互相影响。如果你需要拷贝的对象中没有引用类型,那么对于Golang而言使用浅拷贝就可以
for range循环,for里append
1 | s := []int{1,2,3,4,5} |
不会死循环,for range其实是golang的语法糖,在循环开始前会获取切片的长度 len(切片),然后再执行len(切片)次数的循环。
读写已经关闭的channel会怎样
读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。
如果 chan 关闭前,buffer 内有元素还未读 , 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。
如果 chan 关闭前,buffer 内有元素已经被读完,chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false。
因为如果接收值的地址 ep 不为空
那接收值将获得是一个该类型的零值
typedmemclr 会根据类型清理相应地址的内存
这就解释了上面代码为什么关闭的 chan 会返回对应类型的零值
写已经关闭的 chan 会 panic,
因为源码当 c.closed != 0 则为通道关闭,此时执行写,源码提示直接 panic,输出的内容就是上面提到的 “send on closed channel”
对未初始化的channel进行读写会怎样
读写未初始化的 chan 都会阻塞
未初始化的 chan 此时是等于 nil,当它不能阻塞的情况下,直接返回 false,表示读写 chan 失败
当 chan 能阻塞的情况下,则直接阻塞 gopark方法,读写抛出错误
for循环select
如果通道已经关闭会怎么样?如果select中的case只有一个,又会怎么样?
1 | c := make(chan int) |
for循环select时,如果其中一个case通道已经关闭,则每次都会执行到这个case。
怎样不读关闭的channel,根据channel读关闭的会返回false,根据false判断
x, ok := <-c 返回的值里第一个x是通道内的值,ok是指通道是否关闭,当通道被关闭后,ok则返回false,因此可以根据这个进行操作x, ok := <-c 返回的值里第一个x是通道内的值,ok是指通道是否关闭,当通道被关闭后,ok则返回false,因此可以根据这个进行操作
执行c = nil 将通道置为nil,相当于读一个未初始化的通道,则会一直阻塞, seleect会跳过这个阻塞
如果select里边只有一个case,而这个case被关闭了,则会出现死循环。
如果像上面置为nil去解决,则会引发别的问题
第一次读取case能读到通道里的10
第二次读取case能读到通道已经关闭的信息。此时将通道置为nil
第三次读取case时main协程会被阻塞,此时整个进程没有其他活动的协程了,进程deadlock
goalng内存逃逸
golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 逃逸 了,必须在堆上分配
在一个切片上存储指针或带指针的值。 一个典型的例子就是 []* string
在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出
slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配
怎样避免内存逃逸
在runtime/stubs.go:133有个函数叫noescape。noescape可以在逃逸分析中隐藏一个指针。让这个指针在逃逸分析中不会被检测为逃逸
noescape() 函数的作用是遮蔽输入和输出的依赖关系。使编译器不认为 p 会通过 x 逃逸, 因为 uintptr() 产生的引用是编译器无法理解的。
内存泄漏相关问题
1 | package main |
上面这道题在不执行resp.Body.Close()的情况下,泄漏了吗?如果泄漏,泄漏了多少个goroutine?
不进行resp.Body.Close(),泄漏是一定的,读写加main是13个,是不对的
因为http。Get的DefaultTransport
一次建立连接,就会启动一个读goroutine和写goroutine。这就是为什么一次http.Get()会泄漏两个goroutine的来源
虽然执行了 6 次循环,而且每次都没有执行 Body.Close() ,就是因为执行了ioutil.ReadAll()把内容都读出来了,连接得以复用,因此只泄漏了一个读goroutine和一个写goroutine,最后加上main goroutine,所以答案就是3个goroutine。
从另外一个角度说,正常情况下我们的代码都会执行 ioutil.ReadAll(),但如果此时忘了 resp.Body.Close(),确实会导致泄漏。但如果你调用的域名一直是同一个的话,那么只会泄漏一个 读goroutine 和一个写goroutine,这就是为什么代码明明不规范但却看不到明显内存泄漏的原因
goroutine泄露问题
如果你启动了一个 goroutine,但并没有符合预期的退出,直到程序结束,此goroutine才退出,这种情况就是 goroutine 泄露。当 goroutine 泄露发生时,该 goroutine 的栈(一般 2k 内存空间起)一直被占用不能释放,goroutine 里的函数在堆上申请的空间也不能被 垃圾回收器 回收。这样,在程序运行期间,内存占用持续升高,可用内存越来也少,最终将导致系统崩溃
channel引起泄漏
1.从 channel 里读,但是没有写
2.向 unbuffered channel 写,但是没有读
3.向已满的 buffered channel 写,但是没有读
解决方法,2,3设定buffer size,分配值和接收值数量一样
4.select操作在所有case上阻塞
解决方法:
使用上述实现里的模式,传入一个 quit channel,配合 select,当不需要的时候,close 这个 quit channel,该 goroutine 就可以退出
或者使用ctx包
5.goroutine进入死循环中,导致资源一直无法释放
interface类型可以比较吗
interface类型可以比较,动态类型,动态值,两个一样就可以比较,只有slice,map无法比较,只能和nil比较,或者通过reflect包DeepEqual,或者cmp包比较
struct可以比较,但是必须是同一类型的实例值可以比较,都相等为true
map如何顺序读取
map不能顺序读取,是因为他是无序的,想要有序读取,首先的解决的问题就是,把key变为有序,所以可以把key放入切片,对切片进行排序,遍历切片,通过key取值
实现set
通过map实现
1 | type inter interface{} |
switch fullthrough
switch的每个case最后都会break
用fullthrough关键字,可以继续执行下一个case
go的几个命令
1 | go env: #用于查看go的环境变量 |
go的log包线程安全
Golang的标准库提供了log的机制,但是该模块的功能较为简单(看似简单,其实他有他的设计思路)。在输出的位置做了线程安全的保护。