Go语言基础篇
go的包管理工具
![image][go_package]
- Gopath < go 1.5. 2009.11.10 随go语言诞生,通过统一包存放的路径实现包管理,不支持依赖包的版本控制。
Gopath模式和Gopath路径的区别:Gopath模式是指我们通过Gopath来管理我们的包,Gopath路径指的是GOPATH这个环境变量的路径。 GoRoot目录是Go语言的安装路径,GoPath跟GoRoot不能是同一个目录。
go get: 1.先将远程代码克隆到 G O P A T H / s r c 目录下 2. 执行 g o i n s t a l l 命令。可以指定 − d 参数仅下载不安装。 ∗ ∗ g o i n s t a l l : ∗ ∗ 包如果可生成可执行的二进制文件,则存储在 GOPATH/src目录下 2.执行go install命令。可以指定-d参数仅下载不安装。 **go install:** 包如果可生成可执行的二进制文件,则存储在 GOPATH/src目录下2.执行goinstall命令。可以指定−d参数仅下载不安装。∗∗goinstall:∗∗包如果可生成可执行的二进制文件,则存储在GOPATH/bin 普通包,会将编译生成.a结尾的文件放到 G O P A T H / p k g 下作为编译缓存加快后续的编译速度。如何判断一个包能否生成可执行文件呢?在 g o 语言中 m a i n 包中存在 m a i n 函数的情况下,才能生成可执行文件。 g o i n s t a l l 是建立在 G O P A T H 上的无法单独使用 g o i n s t a l l 生成的可执行文件的名称与包名一致 g o i n s t a l l 输出目录是不支持通过命令指定的。 g o 1.15 版本以后如果没有本地包, g o i n s t a l l 也会从远程下载依赖包。 ∗ ∗ g o b u i l d : ∗ ∗ 默认会在当前目录下编译生成可执行文件,可通过参数指定安装目录。不会将可执行文件复制到 GOPATH/pkg下 作为编译缓存加快后续的编译速度。 如何判断一个包能否生成可执行文件呢? 在go语言中 main包中存在main函数的情况下,才能生成可执行文件。 go install是建立在GOPATH上的 无法单独使用 go install生成的可执行文件的名称与包名一致 go install输出目录是不支持通过命令指定的。 go1.15版本以后 如果没有本地包,go install也会从远程下载依赖包。 **go build:** 默认会在当前目录下编译生成可执行文件,可通过参数指定安装目录。不会将可执行文件复制到 GOPATH/pkg下作为编译缓存加快后续的编译速度。如何判断一个包能否生成可执行文件呢?在go语言中main包中存在main函数的情况下,才能生成可执行文件。goinstall是建立在GOPATH上的无法单独使用goinstall生成的可执行文件的名称与包名一致goinstall输出目录是不支持通过命令指定的。go1.15版本以后如果没有本地包,goinstall也会从远程下载依赖包。∗∗gobuild:∗∗默认会在当前目录下编译生成可执行文件,可通过参数指定安装目录。不会将可执行文件复制到GOPATH/bin目录下。
go run: 编译并运行go文件,一般在开发过程中测试使用。 go run不依赖GOPATH。 只能编译可执行的go文件 即main包中存在main函数的go文件。 - GOPATH目录: src存放源代码文件 pkg存放编译后的文件 bin存放编译后的可执行文件
- GoVendor >= go 1.5
- Go Modules >= Go1.11 2018年8月 go1.11版本发布。Go1.13版本后 Go Modules默认开启
- Go Moudles具体用法 1.环境准备 Go版本>1.11 2.设置go代理 3.初始化工程:go mod init {go Modules的名称}
- go mod 常用命令 go mod init:初始化go module工程 2. go mod tidy:解决工程中包的依赖关系 3.go mod download:下载依赖包到本地缓存里。
go.mod文件
![image][go_mod]
![image][go_fake_version]
go.sum文件
- 详细罗列了当前项目直接或间接依赖的所有模块的版本
![image][go_sum_1]
go.sum是怎么做包校验的?
- 若本地缓存有依赖包,计算包的hash并与go.sum记录对比
- 依赖包版本中任何一个文件 (包括go.mod)的改变都会改变hash
- 哈希值是由算法SHA-256计算出来的
- 校验目的是保证项目所依赖的那些模块版本不会被篡改
- 公网可下载的包会去Go校验数据库获取模块的校验和(sum.golang.org/sum.golang.google.cn)
不会对依赖包做哈希校验的情况
- GOPRIVATE匹配到的包
- 打包到vendor目录中的包
- GOSUMDB设置为off
同一个模块版本的数据只会缓存一份,所有其他模块共享使用
清除包缓存:go clean -modcache
为什么go.sum中版本数量会比go.mod多呢?
- go.mod只在依赖包不含go.mod文件时才会记录间接依赖包版本
- go.sum则是要记录构建用到的所有依赖包版本
关闭依赖包校验
- go env -w GOSUMDB=off
- 或者将环境变量添加到/etc/profile || export GOSUMDB=off(unix环境)
依赖包的存储
- GOPATH模式下,依赖包存储在$GOPATH/src 只存放当前依赖版本的文件 包含.git目录
- GOMODULE模式下,依赖包存储在$GOPATH/pkg/mod,存放各个版本的依赖包 只包含包文件 不包含.git目录
如何使用内部包?
internal文件夹内的包
- go 1.4版本开始 可以使用internal文件限制包的导入权限
- internal文件夹内的包只能被其父目录下的包或子包导入
go语言中函数和变量的可见性
- 小写字母开头的函数、变量、结构体只能在本包内访问
- 大写字母开头的函数、变量、结构体可以在其他包访问
公司内部开发的私有包
- 通过本地包的方式导入
![image][go_local_pkg]
- 通过私有仓库的方式导入
GOPRIVATE 在go1.13版本后开始支持,可以使用域名 也可以使用通配符。
GONOPROXY 也不通过GOPROXY拉取。一般和GONOSUMDB搭配。GONOSUMDB=$GONOPROXY匹配到的包都不去校验。
通过GOPRIVATE拉取的包,自动地不去校验。
- 工作区模式
init()函数是什么时候执行的?
init()函数的作用是什么?
- 程序执行前包的初始化
- 同一个go文件中 可以定义多个同名的init()函数,这一点和普通函数不一样。
init()函数的执行顺序
- 在同一个Go文件中的多个init方法,按照代码顺序依次执行
- 同一个package中,按照文件名的顺序执行
- 不同package且不相互依赖,按照import顺序执行
- 不同package且相互依赖,最后被依赖的最先被执行
init()函数的注意事项
- 一个包被引用多次,这个包的init函数只会执行一次
- 所有init函数都在同一个goroutine内执行
- 注意包的循环导入问题
go文件的初始化顺序
- 引入的包
- 当前包中的常量 变量
- 当前包的init
- main函数
go语言如何获取项目的跟目录
- os.Getwd() 打印的是当前命令的执行路径
- os.Args[0]
- os.Executable
- runtime.Caller
![image][go_root_path]
- 环境变量:1.通过GOPATH或者自定义项目根路径的环境变量 2.利用系统自带的环境变量和路径
Go输出时%v %+v %#v有什么区别?
go语言中各种类型的输出
- go语言中一个中文占3个字符
![image][go_print]
![image][go_print_float]
![image][go_print_string]
![image][go_print_pointer]
fmt的几个print函数的区别
![image][go_fmt_diff]
%v %+v %#v
- %v只输出结构体的所有的值,不会输出结构体的字段名
- %+v不仅会输出结构体的值,还会输出结构体的字段名
- %#v还会输出结构体的名字以及字段中指针类型的名称
go语言处理输入
![image][go_input_1]
![image][go_input_2]
Go语言中new和make有什么区别?
-
make不仅分配内存,还会初始化。new只会分配零值填充的值。
-
make只适用于slice,map,channel的数据,new没有限制。
-
make返回元素类型(T),new返回类型的指针(*T)。
new和make函数在源码中的定义
func make(t Type, size...IntegerType) Type func new(Type) *Type
总结
- new可以为任何类型的值开辟内存并返回此值的指针。
- new申请的值均为零值,对创建映射和切片没有意义。
- 实际工作中通常使用字面量来创建数组而很少使用new。
切片和数组的区别
数组和切片的相同点
- 数组和切片都要求全部元素的类型都必须相同
- 数组和切片的所有元素都是紧挨着存放在一块连续的内存中
slice和map的区别
- map中的所有元素所在内存连续但不一定紧邻
- 访问元素的时间复杂度都是O(1),但map更慢
map的优势
- map的key值类型可以是任何可比较的类型
- 对于大多数元素为零值的情况,map可节省大量内存
数组和切片的零值不同
- 数组的零值是每个元素类型的零值
- 切片的零值为nil
- 指针类型的数组和切片直接用类型声明后是nil,不能直接使用
![image][go_zero_1]
![image][go_zero_2]
数组和切片的存储形式
![image][go_array_slice_memory]
切片的赋值过程
数组和切片的字面量表示方式
//切片的字面量有下面几种表示方式:
s1 := []string{"a", "b", "c"}
fmt.Println("s1:", s1)
s2 := []string{0: "a", 1: "b", 2: "c"}
fmt.Println("s2:", s2)
s3 := []string{2: "c", 1: "b", 0: "a"}
fmt.Println("s3:", s3)
s4 := []string{2: "c", 0: "a", "b"}
fmt.Println("s4:", s4[1])
//1.通过make申请
var s6 = make([]int, 100)
//2.初始化第100个元素
var s7 = []int{99: 0}
//
3.先创建一个容量为100的数组取其地址后再截取全部元素作为切片
var s8 = (&[100]int{})[:]
//
4.通过new创建一个容量为100的数组,再取全部元素
var s9 = new([100]int)[:]
100 100 100 100
println(len(s6), len(s7), len(s8), len(s9)) //100 100 100 100
//数组的字面量有下面几种表示方式:
a1 := [4]string{"a", "b", "c", "d"}
fmt.Println("a1:", a1)
//
a2 := [4]string{0: "a", 1: "b", 2: "c", 3: "d"}
fmt.Println("a2:", a2)
//
a3 := [4]string{1: "b", "a", "c"}
fmt.Println("a3:", a3)
fmt.Println("a3:","a3[0]:", a3[0], "a3[2]:",a3[2]," a3[3]:", a3[3])
//如果不指定下标,将会从前一个下标开始递增
a4 := [4]string{1: "b", "a", "c", "d"} //编译错误
//这里元素c的下标也是1,而数组和切片的下标是不能重复的,即元素键值不能重复,但元素值可以重复
a5 := [4]string{1: "b", 0: "a", "c", "d"}//编译错误
a6 := [...]string{"a", "b", "c", "d"}
a6[4] = "4"
fmt.Println("a6:", a6)
a7 := [...]string{3: "d", 1: "b", "a"}
fmt.Println("a7:", a7, a7[2])
//编译错误
a8 := [...]string{3: "d", 1: "b", "a", "c"}//编译错误
//大于等于0的常量整型可以作为数组和切片的下标,但变量不行
const x uint = 1
var y uint = 1
a8 := []string{0: "a", x: "b", 2: "c"}
fmt.Println("a8:", a8)
a9 := []string{0: "a", y: "b", 2: "c"} //编译错误
数组的比较
- 不同长度的数组不可比较
- 数组不能跟nil比较,切片可以
- 切片之间不能比较,但相同长度的数组可以
go语言中双引号和反引号有什么区别
go语言数据类型
![image][go_zero_1]
![image][go_zero_2]
go语言中的单引号
- Go语言中没有专门的字符类型,单个字符一般使用byte来声明
- 单引号用来定义一个byte或者rune
- byte是uint8类型的别名,而rune是int32类型的别名
双引号和反引号的区别
- 双引号的字符串支持转义,而反引号的字符串不支持转义
双引号定义字符串的两种表示方式
- 字符串字面量
- rune字面量
//a,b,c的ASCII码值的十进制分别是97,98,99,对应的8进制为141,142,143
//对于字符串abc我们可以通过
s3 := "\141\142\143"
fmt.Println("s3:", s3, reflect.TypeOf(s3))
//a,b,c的ASCII码值的十进制分别是97,98,99,对应的16进制为61,62,63
s4 := "\x61\x62\x63"
fmt.Println("s4:", s4, reflect.TypeOf(s4))
//unicode也是通过16进制的码值表示的
s5 := "\u0061\u0062\u0063"
fmt.Println("s5:", s5, reflect.TypeOf(s5))
s6 := "\U00000061\U00000062\U00000063"
fmt.Println("s6:", s6, reflect.TypeOf(s6))
- 字符串字面量不支持单引号转义
- 双引号字符串中不能包含单引号和双引号八进制ASCII码值
- 双引号字符串中允许单引号和双引号的16进制ASCII码值
字符串相关考点
字符串的转义
字面量的表示
格式化输出
字符串的截取替换等操作
正则匹配
字符串与数值之间的转换
零拷贝复制
strings.TrimRight 和 strings.TrimSuffix有什么区别?
func main() {
var s = "gogo123go面试专题goabcgogo"
p := fmt.Println
p("1:", strings.TrimPrefix(s, "go")) //go123go面试专题goabcgogo
p("2:", strings.TrimSuffix(s, "go")) // gogo123go面试专题goabcgo
p("3:", strings.TrimLeft(s, "go")) // 123go面试专题goabcgogo
p("4:", strings.TrimRight(s, "go")) // gogo123go面试专题goabc
p("5:", strings.Trim(s, "go")) // 123go面试专题goabc
p("6:", strings.TrimFunc(s, func(r rune) bool {
return r < 128 // trim all ascii chars
})) // 面试专题
}
- trim = TrimLeft + TrimRight
- Trim, TrimLeft, TrimRight是找到不满足条件的字符串为止
- TrimPrefix 和 TrimSuffix是只要找到一个满足条件的就终止
数值类型运算后值溢出会发生什么?
值溢出问题
- 无符号整型溢出时,值为0
- 有符号整型的溢出,值出现反转
- 数值类型转换时溢出
- 整型转无符号整型时,要考虑符号丢失的问题。
- 移位操作时要考虑位数是否会溢出。
- 浮点型溢出后为最大值
- 浮点数不能被移位
精度问题
- 在指定范围内,将n位十进制数(按照科学计数法表示)与二进制数互转,如果数据不发生损失,就意味着在此范围内有n位精度。
- 浮点数不满足结合律
iota计数器
- const中每新增一行常量声明将使iota计数一次。
- iota在const关键字出现时将被重置为0
- 当且仅当iota出现在const声明中的第一行时,iota常量值才会从0开始。
const (
_ = iota //将0号计数占位,后面从1开始
KB = 1 << (10 * iota) // <<移位操作,速度比乘除法快
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
Go语言中每个值在内存中只分布在一个内存块上的类型有哪些?
内存块
- 内存块就是一段在运行时刻承载着若干值部的连续内存片段
- 一个内存块可承载的值部可能不止一个
内存块什么时候开辟?
- 拼接非常量字符串时
- 字符串转字节切片或者字节切片转字符串的时候
- 将一个整数转换为字符串
- 调用内置append函数触发切片扩容时
- 向map中添加元素,且map底层内部的哈希表需要改变容量时
内存块在哪里开辟?
- 栈,本质上是一个预申请的内存段由协程维护,初始化最小2KB。
- 协程栈尺寸有最大限制,64位系统512MB,32位系统128MB。
- 开辟在栈上的内存块只能在此协程内部使用,与其他协程隔离。
- 内存块也可以开辟在堆上,堆是个虚拟概念,每个程序只有一个堆。
内存块会开辟在堆上?还是会开辟在栈上呢?
- 内存块会开辟的位置是由编译器来决定的,如果编译器不能确定某块内存只会被一个协程访问,那编译器就会将这个内存块开辟在堆上。编译器采取一种保守且安全的策略。
开辟在栈上的内存块与开辟在堆上的内存块相比有哪些优势呢?
- 从栈上开辟内存块速度会更快。
- 栈上开辟的内存块不需要被垃圾回收。
- 栈上的内存块的访问对CPU缓存更加友好。
内存逃逸
- 如果一个局部变量的某些值部被开辟到了堆上,我们就认为这个局部变量发生了内存逃逸。
- 逃逸到堆上的值部至少会被一个开辟在栈上的值部所引用
- 包级别的变量都被开辟到了堆上,并被一个全局内存区的隐式指针所引用。
Go值的直接值部和间接值部
- 分布在不同内存块上的值就有直接值部和间接值部
- 被直接值部引用的值就是间接值部
- 直接值部是每个值只分布在一个内存块的类型,包括布尔、数值、指针、结构体和数组类型。
- 间接值部是被一个或者多个直接值部引用的值,会分布在不同的内存块上
- 包括切片类型,映射类型,通道类型,函数类型,接口类型,字符串类型。
Go语言中哪些类型可以被内嵌?
结构体的类型内嵌
- 内嵌,其实就是字段只包含类型不包含字段名。
- 一个指针类型*T只有在T为一个类型名并且T既不表示一个指针类型,也不表示一个接口类型的时候才能被用作内嵌字段。
type s1 struct {
// 可以被内嵌的类型
m
*m
Int
*Int
IPerson
Person
*Person
PersonPtr
int
*int
//不能被内嵌的类型:
IntPtr //具名指针类型
*IntPtr //基类型IntPtr为指针类型
AliasIntPtr //基类型AliasIntPtr为指针类型
*PersonPtr //基类型PersonPtr为指针类型
*IPerson //基类型IPerson为接口类型
[]int //无名非指针类型
map[string]int //无名非指针类型
func() //无名非指针类型
}
内嵌的意义
- 类似于面向对象编程中的继承。
[go_datatype]: