目录
一、环境搭建
1.windows安装
2.linux安装
3.开发工具
二、变量定义与输入输出
1.变量定义
2.全局变量与局部变量
3.定义多个变量
4.常量定义
5.命名规范
6.输出
7.输入
三、基本数据类型
1.整数型
2.浮点型
3.字符型
4.字符串类型
转义字符
多行字符串
5.布尔类型
四、数组、切片、map
1.数组
2.切片
make函数
切片面试题
3.Map
map面试题
一、环境搭建
官网
https://go.dev/dl/
访问不了的就访问中文网就好了
go安装包下载
https://studygolang.com/dl
安装指定版本的安装包就好了
1.windows安装
选择 xxx.windows-amd64.msi
- 将go的对应bin目录设置为环境变量,这一步是方便可以在命令行里面直接使用go命令
- 将go的第三方bin目录设置为环境变量,一般是在用户目录下,这一步是为了以后使用go install安装的第三方可执行文件可以直接使用
2.linux安装
选择 xxx.linux-amd64.tar.gz
该站点比较快:All releases - The Go Programming Language
# 下载
wget https://golang.google.cn/dl/go1.22.2.linux-amd64.tar.gz
# 解压
tar -xvf go1.22.2.linux-arm64.tar.gz -C /usr/local
# 配置环境变量
echo 'export GO111MODULE=on' >> /etc/profile
echo 'export GOROOT=/usr/local/go' >> /etc/profile
echo 'export GOPATH=/home/gopath' >> /etc/profile
echo 'export PATH=$PATH:$GOROOT/bin:$GOPATH/bin' >> /etc/profilesource /etc/profile
# 创建go包安装目录
mkdir -p /home/gopath
# 设置代理
go env -w GOPROXY=https://goproxy.cn,direct
# 检查成功
go version
3.开发工具
推荐goland
二、变量定义与输入输出
1.变量定义
package mainimport "fmt"func main() {// 先定义,再赋值var name stringname = "os_lee1"fmt.Println(name)// 定义加赋值var userName string = "os_lee1"fmt.Println(userName)
}
如果一个变量定义了,但是没有赋值,那么这个变量的值就是这个类型的 "零值"
// 变量类型省略
var name = "os_lee"// 简单声明
name := "os_lee"
2.全局变量与局部变量
定义在函数体(包括main函数)内的变量都是局部变量,定义了就必须使用
定义在外部的变量就是全局变量,可以只定义不使用
package mainimport "fmt"// 全局变量可以不使用
var userName = "oslee_全局" func main() {// 局部变量var name = "oslee_局部"// 在函数体内定义的变量,必须要使用fmt.Println(name)
}
3.定义多个变量
package mainimport "fmt"func main() {var name1, name2, name3 string // 定义多个变量var a1, a2 = "os", "lee" // 定义多个变量并赋值a3, a4 := "os", "lee" // 简短定义多个变量并赋值fmt.Printf("name1: %s, name2: %s, name3: %s\n", name1, name2, name3)fmt.Printf("a1: %s, a2: %s, a3: %s, a4: %s\n", a1, a2, a3, a4)
}
package mainimport "fmt"var (name string = "os"userName = "os_lee"
)func main() {fmt.Println(name, userName)
}
4.常量定义
定义的时候就要赋值
赋值之后就不能再修改了
package mainimport "fmt"const name string = "os_lee" // 定义就要赋值func main() {fmt.Println(name)
}
5.命名规范
核心思想:首字母大写的变量、函数。方法,属性可在包外进行访问
6.输出
package mainimport "fmt"func main() {fmt.Println("os_lee")fmt.Println(1)fmt.Println(true)fmt.Println("什么", "都", "可以", "输出")
}
格式化输出
package mainimport "fmt"func main() {fmt.Printf("%v\n", "你好") // 可以作为任何值的占位符输出fmt.Printf("%v %T\n", "os", "lee") // 打印类型fmt.Printf("%d\n", 3) // 整数fmt.Printf("%.2f\n", 1.25) // 小数fmt.Printf("%s\n", "哈哈哈") // 字符串fmt.Printf("%#v\n", "") // 用go的语法格式输出,很适合打印空字符串// 还有一个用的比较多的就是将格式化之后的内容赋值给一个变量name := fmt.Sprintf("%v", "你好")fmt.Println(name)
}
7.输入
package mainimport "fmt"func main() {fmt.Println("输入您的名字:")var name stringfmt.Scan(&name) // 这里记住,要在变量的前面加个&, 后面讲指针会提到fmt.Println("你输入的名字是", name)
}
三、基本数据类型
go语言的基本数据类型有
- 整数形
- 浮点型
- 复数
- 布尔
- 字符串
1.整数型
go语言的整数类型,具体细分有很多
var n1 uint8 = 2
var n2 uint16 = 2
var n3 uint32 = 2
var n4 uint64 = 2
var n5 uint = 2
var n6 int8 = 2
var n7 int16 = 2
var n8 int32 = 2
var n9 int64 = 2
var n10 int = 2
大家只需要记住以下几点
- 默认的数字定义类型是int类型
- 带个u就是无符号,只能存正整数
- 后面的数字就是2进制的位数
- uint8还有一个别名 byte, 一个字节=8个bit位
- int类型的大小取决于所使用的平台
例如uint8,那就是8个二进制位,都用来存储数据,那最小就是0,最大就是2的八次方-1=255
那int8,因为要拿一位存符合,使用实际只有七位可用,所以最小的就是负2的七次方=-128,最大的就是2的七次方-1=127
至于为什么要减一,其实很好理解,因为实际到最后一个数字的时候,已经向前进位了,例如一个小时是60分钟,但是分钟最大只有59
第五点的测试
我是64位操作系统,那么我会试一下int是不是就是int64的最大上限
2的63次方-1=9223372036854775807fmt.Printf("%.0f\n", math.Pow(2, 63))
var n1 int = 9223372036854775807
fmt.Println(n1)
var n2 int = 9223372036854775808 // 看它报不报错
fmt.Println(n2)
2.浮点型
Go语言支持两种浮点型数:float32 和 float64
- float32 的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32
- float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64
如果没有显式声明,则默认是float64
3.字符型
注意哦,是字符,不是字符串
比较重要的两个类型是byte(单字节字符)和rune(多字节字符)
package mainimport "fmt"func main() {var c1 = 'a'var c2 = 97fmt.Println(c1) // 直接打印都是数字fmt.Println(c2)fmt.Printf("%c %c\n", c1, c2) // 以字符的格式打印var r1 rune = '中'fmt.Printf("%c\n", r1)
}
在 Go 中,字符的本质是一个整数,直接输出时,是该字符对应的 UTF-8 编码的码值
可以直接给某个变量赋一个数字,然后按格式化输出时 %c ,会输出该数字对应的 unicode 字符
字符类型是可以进行运算的,相当于一个整数,因为它都对应有 Unicode 码。
4.字符串类型
和字符不一样的是,字符的赋值是单引号,字符串的赋值是双引号
var s string = "lee李"
fmt.Println(s)
转义字符
一些常用的转义字符
fmt.Println("枫枫\t知道") // 制表符
fmt.Println("枫枫\n知道") // 回车
fmt.Println("\"枫枫\"知道") // 双引号
fmt.Println("枫枫\r知道") // 回到行首
fmt.Println("C:\\pprof\\main.exe") // 反斜杠
多行字符串
在``这个里面,再出现转义字符就会原样输出了
package mainimport "fmt"func main() {str := `今天
天气
真好
`fmt.Println(str)
}
5.布尔类型
- 布尔型数据只有 true(真)和 false(假)两个值
- 布尔类型变量的默认值为false
- Go 语言中不允许将整型强制转换为布尔型
- 布尔型无法参与数值运算,也无法与其他类型进行转换
零值问题
如果我们给一个基本数据类型只声明不赋值,那么这个变量的值就是对应类型的零值,例如int就是0,bool就是false,字符串就是""
package mainimport "fmt"func main() {var a1 intvar a2 float32var a3 stringvar a4 boolfmt.Printf("%#v\n", a1)fmt.Printf("%#v\n", a2)fmt.Printf("%#v\n", a3)fmt.Printf("%#v\n", a4)
}
四、数组、切片、map
1.数组
数组(Array)是一种非常常见的数据类型,几乎所有的计算机编程语言中都会用到它
- 数组里的元素必须全部为同一类型,要嘛全部是字符串,要嘛全部是整数
- 声明数组时,必须指定其长度或者大小
package mainimport "fmt"func main() {var array [3]int = [3]int{1, 2, 3}fmt.Println(array)var array1 = [3]int{1, 2, 3}fmt.Println(array1)var array2 = [...]int{1, 2, 3}fmt.Println(array2)
}
如果要修改某个值,只能根据索引去找然后替换
var array1 = [3]int{1, 2, 3}
array1[0] = 10 // 根据索引找到对应的元素位置,然后替换
fmt.Println(array1)
2.切片
很明显啊,go里面的数组,长度被限制死了,所以不经常用
所以go出了一个数组plus,叫做slice(切片)
切片(Slice)相较于数组更灵活,因为在声明切片后其长度是可变的
package mainimport "fmt"func main() {// 定义一个字符串切片var list []stringlist = append(list, "枫枫")list = append(list, "知道")fmt.Println(list)fmt.Println(len(list)) // 切片长度// 修改第二个元素list[1] = "不知道"fmt.Println(list)
}
make函数
除了基本数据类型,其他数据类型如果只定义不赋值,那么实际的值就是nil
// 定义一个字符串切片
var list []string
fmt.Println(list == nil) // true
那么我们可以通过make函数创建指定长度,指定容量的切片了
make([]type, length, capacity)
package mainimport "fmt"func main() {// 定义一个字符串切片var list = make([]string, 0)fmt.Println(list, len(list), cap(list))fmt.Println(list == nil) // falselist1 := make([]int, 2, 2)fmt.Println(list1, len(list1), cap(list1))
}
为什么叫切片?
因为切片是数组切出来的
package mainimport "fmt"func main() {var list = [...]string{"a", "b", "c"}slices := list[:] // 左一刀,右一刀 变成了切片fmt.Println(slices)fmt.Println(list[1:2]) // b
}
切片排序
package mainimport ("fmt""sort"
)func main() {var list = []int{4, 5, 3, 2, 7}fmt.Println("排序前:", list)sort.Ints(list)fmt.Println("升序:", list)sort.Sort(sort.Reverse(sort.IntSlice(list)))fmt.Println("降序:", list)
}
切片面试题
面试题1:什么是Go语言中的切片?
答案: Go语言中的切片(slices)是一种灵活的、动态大小的、基于数组的抽象数据类型。它并不存储任何数据,而是指向底层数组的一个片段,包含了三个信息:指向数组的指针、长度(len)和容量(cap)。长度表示切片当前拥有的元素个数,容量则是切片可以扩展到的最大元素个数,容量等于或大于长度。
面试题2:切片的扩容是如何工作的?
答案: 当向切片添加元素超出其当前容量时,Go语言会自动扩容切片。扩容的具体策略并不是固定的,但大致遵循以下规则:
- 如果切片的容量不足以容纳新增元素,Go 会创建一个新的更大的底层数组,将原有数据复制到新数组,并更新切片的指针和容量。
- 扩容因子通常为原来容量的2倍,但是也可能会根据实际情况调整,比如在Go 1.17版本之后,根据内存分配器的行为,扩容可能会跳跃式增长以更好地适应内存分配器的粒度。
- 新容量至少会增加到原来的两倍+所需添加的元素数量,以尽量减少频繁的扩容操作。
面试题3:切片与数组有什么区别?
答案:
长度可变性:
- 数组(array)的长度在声明时确定并且不能改变。
- 切片(slice)虽然在声明时可以指定初始长度,但可以在运行时动态改变其长度(增删元素),不过容量有限制。
存储结构:
- 数组是一个定长的、连续的内存区域。
- 切片不是数据结构,它只是一个描述符,指向一个数组的一部分。
引用行为:
- 数组变量直接存储数据,赋值操作会复制整个数组的内容。
- 切片变量存储的是指向数组的指针和长度、容量信息,赋值操作只会复制切片描述符,不会复制底层数据。
面试题4:浅拷贝和深拷贝
浅拷贝(Shallow Copy): 浅拷贝是创建一个新的切片,但它仍然指向同一个底层数组。这意味着对新切片所做的修改会影响到原始切片所指向的数据。
// 示例1:直接赋值
original := []int{1, 2, 3}
copied := original // 此时copied是对original的浅拷贝// 示例2:使用内置的copy函数
original := []int{1, 2, 3}
copied := make([]int, len(original))
copy(copied, original) // 这也是浅拷贝,copied和original共享相同的底层数组深拷贝(Deep Copy): 深拷贝是创建一个与原始切片完全独立的新切片,包含一个全新的底层数组。对新切片所做的修改不会影响到原始切片。
// 手动循环遍历复制每一个元素 original := []int{1, 2, 3} copied := make([]int, len(original)) for i, v := range original { copied[i] = v }// 或者使用反射(reflect)包,但请注意这不是最优实践,仅作演示 import "reflect" original := []int{1, 2, 3} copied := reflect.ValueOf(original).Clone().Interface().([]int)
3.Map
- Go语言中的map(映射、字典)是一种内置的数据结构,它是一个
无序
的key-value对的集合- map的key必须是基本数据类型,value可以是任意类型
- 注意,map使用之前一定要初始化
package mainimport "fmt"func main() {// 声明var m1 map[string]string// 初始化1m1 = make(map[string]string)// 初始化2m1 = map[string]string{}// 设置值m1["name"] = "枫枫"fmt.Println(m1)// 取值fmt.Println(m1["name"])// 删除值delete(m1, "name")fmt.Println(m1)// 声明并赋值var m2 = map[string]string{}fmt.Println(m2)var m3 = make(map[string]string)fmt.Println(m3)}
map取值
- 如果只有一个参数接,那这个参数就是值,如果没有,这个值就是类型的零值
- 如果两个参数接,那第二个参数就是布尔值,表示是否有这个元素
package mainimport "fmt"func main() {// 声明并赋值var m1 = map[string]int{"age": 21,}age1 := m1["age1"] // 取一个不存在的fmt.Println(age1)age2, ok := m1["age1"]fmt.Println(age2, ok)
}
map面试题
面试题1:什么是Go语言中的map?
答案: Go语言中的map是一种关联数组或字典类型的数据结构,它存储键值对(key-value pairs),通过键(key)快速查找对应的值(value)。键和值可以是任何类型,但键的类型必须支持相等比较,通常为整型、浮点型、字符串或复合类型(如结构体,但结构体内的字段必须支持相等比较)。
面试题2:Go语言中的map何时会引发panic?
答案:
- 在使用尚未初始化的map时(即nil map)执行读写操作,会导致panic。
- 在迭代map的过程中,如果同时修改该map,也会导致panic,除非使用
for range
循环迭代并在循环体内使用delete
函数删除元素。面试题3:Go语言中map的扩容是如何进行的?
Go语言中的map在底层实现上使用了哈希表。当map中的元素数量越来越多,达到一定的负载因子时,Go语言会自动触发map的扩容操作。扩容是为了保持map操作的高效性,防止哈希冲突过于密集,导致性能下降。
扩容的具体流程如下:
负载因子判断: 当map的元素数量(entry count)与其桶(bucket)数量的比例超过一定阈值时(通常大约是6.5),map会触发扩容。这个阈值可以通过
runtime
包的内部常量mapExpandHeapSize
和mapExpandLoadFactor
间接推算出来。新桶分配: 扩容时,Go会创建一个新的、大小翻倍的哈希表。例如,如果原map的桶数量是2^N,则新表的桶数量将是2^(N+1)。
元素迁移: Go采用了渐进式(incremental)的迁移策略,不会一次性将所有元素从旧表迁移到新表。在每次map的读写操作时,如果发现正在进行扩容操作,就会顺带将旧表中的部分元素迁移到新表中。每次操作最多迁移两个桶(bucket)的数据。
保持引用关系: 在扩容过程中,map的旧表和新表会同时存在,直到所有元素都迁移到新表为止。旧表的最后一个桶会存储一个指向新表的指针,以确保在扩容过程中依然能正确找到已迁移的元素。
空间回收: 所有元素都迁移到新表后,旧表的空间最终会被垃圾回收机制释放。
值得注意的是,上述细节基于Go语言的早期版本,Go语言的map扩容机制在不同版本间可能会有所调整。最新的Go版本可能会根据具体情况采用不同的扩容策略和负载因子阈值。