golang基础面试题(1)
Go包管理的方式有哪些
- GOPATH: go version < Go1.5
通过统一包存放的路径实现包管理;不支持依赖包的版本控制。
GOPATH 模式:是指我们通过GOPATH来管理我们的包。
GOPATH路径:指的是GOPATH这个环境变量的路径。
独立用户GOPATH配置,不同开发者使用不同版本的GO, 可以设置GOPATH在 ~/.bash_profile文件中;如果要全局生效,配置到/etc/profile文件中。
GOPATH模式下,工程代码必须放在GOPATH/src目录下。
- go get: 先将远程代码clone到本地$GOPATH/src目录下;再执行go install命令,安装生成可执行命令保存到$GOPATH/bin目录;使用制定参数-d可以只是下载,不进行安装。
- go install: 生成可执行而进行文件,存在$GOPATH/bin下;普通包编译生成.a后缀文件保存到$GOPATH/pkg下,提升编译速率。(main包中存在main函数情况下,才可以生成可执行文件)
- go build: 当前目录下编译生成可执行文件;不会将可执行文件拷贝到$GOPATH/bin目录下。
- go run: 编译运行go文件;不依赖go path; 只能编译可执行go文件。
➜ block ll $GOPATH
drwxr-xr-x 28 896 10 16 2023 bin/ // 可执行文件
drwxr-xr-x 7 224 6 23 2023 pkg/ // go install编译后的文件
drwxr-xr-x 20 640 10 7 2023 src/ // 源代码文件
Godep
诞生于2013年,社区开发第一个包管理工具;通过扫描并记录版本控制的信息,再配合go命令加壳实现;
源码保存到Godeps/workspace 并作为GOPATH使用。
Glide
诞生于2014年,通过glide.yaml记录包版本依赖信息,通过glide.lock文件追踪每个包的具体修改。
- Go Vender: go version >= Go1.5 (2015)
诞生于2015年,GO15VENDOREXPERIMENT=1开启;go1.6默认开启;go1.7作为功能支持,取消环境变量,go默认包支持管理工具;源码拷贝到vendor目录,维护vendor.json文件,可制定版本。
go get -u github.com/kardianos/govendor
govendor init
goverdor add +external
goverdor remove +unused
包依赖不能重用,使得包冗余度提升。
- Go Modules: go version >= Go1.11 (2018)
于2018年,go1.11版本发布,GO111MODULE=on开启。从go1.13开始,go modules默认开启。
开启Go Module
export GO111MODULE=on (unix环境)
set GO111MODULE=on (windows环境)
go env -w GO111MODULE=on
常规使用命令
go mod init
go mod tidy
内部包使用
内部包包含两种:
- internal文件夹内的包
- 内部开发的包
- 通过本地包方式导入
通过在go.mod文件中,使用关键字replace, 可以将在工程中依赖的具体包路径,替换成本地的包路径。
replace github.com/user_name/remote/pkg => ../local/path/pkg
缺点,通用性不强,各个开发者本地环境不一致。
- 通过私有仓库方式导入
注意设置好GOPROXY和GOPRIVATE变量。
GOPRIVATE可以设置指定域名的包,不走GOPROXY代理拉取代码。从GO.13版本开始生效使用。
GONOSUMDB变量命中规则,拉取包时候,不进行代码依赖版本的sum校验。
init()函数是什么时候执行的
- 程序执行前包的初始化:类似一些中间件默认配置的初始化。
- init()函数的执行顺序:
a. 在同一个go文件中的多个init方法,按照代码顺序依次执行。
b. 同一个package中,按照文件名的顺序执行。
c. 不同package且不相互依赖,按照import顺序执行;相互依赖最后依赖的先执行。
一个包被引用多次,这个包的init函数只会执行一次。
所有的init函数都在同一个goroutine内执行。
new和make的区别
make不仅仅分配内存,还会对类型进行初始化(可能会初始化申请多个内存块);new只会分配零值填充的值。(通常只会申请一个内存块)
make只能用于slice, map, channel类型的数据,new则没有限制;
make会返回原始类型T;new返回类型的指针(*T)。
数组与切片
相同点:
全部元素的类型必须都是相同的;都是紧挨存放在一块连续的内存中。
不同点:
数组的零值是每个元素类型的零值;切片的零值是nil.
指针类型的数组和切片直接用类型声明后是nil, 不能直接使用。
存储形式不同:
数组结构,连续内存空间,长度已知。
切片有指向存储数据的数组内存空间。
切片的赋值和函数传递,都是引用同一份内存数据。
func main() {
str1 := []string{"a", "b", "c"}
str2 := str1
fmt.Printf("str1 %p \n", str1)
fmt.Printf("str2 %p \n", str2)
str(str1)
}
func str(s []string) {
fmt.Printf("func param %p \n", s)
}
// output
str1 0x14000100060
str2 0x14000100060
func param 0x14000100060
每个值在内存中只分布在一个内存块上的类型
内存块就是一段在程序运行时刻保存着若干值部的连续内存片段
内存块申请与开辟
- 变量声明时(make声明,字面量,普通变量声明)
- 拼接非常量字符串
- 字符串与字节切片互相转换
- 将一个整数转换为字符串
- 调用内置的append函数触发切片扩容时
- map结构添加数据,底层需要扩容。
内存块在哪里开辟和申请
栈: 本质是一个预申请的内存段,由协程维护,初始或最小2KB(>= GO1.19版本自适应)
协程栈尺寸有最大限制,64位系统512M,32位系统128M。
开辟在栈上的内存块只能在此协程内部使用,与其他协程隔离。(并发安全)
堆:内存块可在堆上申请,每个程序程序只有一个堆。
可以被多个协程共享使用,注意并发安全问题。
Q: 什么情况下内存块被开辟在堆上,或者是栈上?
A: 内存块会开辟的位置是由编译器来决定。
从栈上开辟的内存块速度会更快(CPU缓存友好);并且栈上开辟内存块不需要被GC,减轻垃圾回收压力。
内存逃逸
如果一个局部变量的某些值,被开辟到了堆上,就认为这个局部变量发生了内存逃逸。
逃逸到堆上的值部至少被一个开辟到栈上的值部所引用。包级别的变量的都被开辟到了堆上,并被一个全局内存区的隐式指针所引用。
直接值部
直接值部是每个值只分布在一个内存块的类型;包括布尔,数值,指针,结构体和数组类型。
间接值部
间接值部是被一个或者多个直接值部引用的值,会分布在不同的内存块上;包括切片类型,映射类型,函数类型,接口类型,字符串类型。
内存块的地址是可能发生改变的
若是发生内存扩容调整,栈上引用的堆上的(间接值部)的地址,就有可能会发生数据同步迁移,对应的内存地址可能发生改变。
GO 的指针
- 一个指针变量本身存储的是一个内存地址
- 一个内存地址在32位系统上占4个字节;在64位上占据8个字节。
- 内存地址一般用整数的16进制表示,比如0xc000012340
在计算机内存中存储的都是01数据,若用16进制表现出来就是0x...的数据。对数据的定义,是由于具体数据使用场景来表达。例如可以是一个程序局部变量存储的地址,或者是一个指向其他内存区域的地址(指针地址)。
当一个变量被声明的时候,go运行时将为此变量开辟一段内存。此内存的起始地址极为变量的地址。
x := new(int) // 声明指针x
y :=x // 将x的地址赋值给y
tmp := 5
x = &tmp // 将tmp地址赋值给x
那些值不可被寻址
- 映射map的元素
- 字符串的字节元素
- 常量包级别的函数以及用作函数值的方法等。
GO中的nil
- 不同数据类型的nil值,对应的内存大小不同 。(常规类型nil值是8字节,slice切片24字节,interface16字节)
- nil 值不一定是可以相互比较的,主要取该数据类型是否可以比较。
- 可以比较的两个nil值也不一定相等。
Go中的map
只有可比较的类型才能作为map中的key。(不可比较类型:slice,map,function, 含有不可比较类型的组合结构)
float类型如果作为map的key, 会有什么问题?
map在操作key的时候,会通过math.Float64bits函数转换,若是两个浮点数相同,则会出现覆盖的情况。