目录
一、环境搭建
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 main
import "fmt"
func main() {
// 先定义,再赋值
var name string
name = "os_lee1"
fmt.Println(name)
// 定义加赋值
var userName string = "os_lee1"
fmt.Println(userName)
}
如果一个变量定义了,但是没有赋值,那么这个变量的值就是这个类型的 "零值"
// 变量类型省略
var name = "os_lee"
// 简单声明
name := "os_lee"
2.全局变量与局部变量
定义在函数体(包括main函数)内的变量都是局部变量,定义了就必须使用
定义在外部的变量就是全局变量,可以只定义不使用
package main
import "fmt"
// 全局变量可以不使用
var userName = "oslee_全局"
func main() {
// 局部变量
var name = "oslee_局部"
// 在函数体内定义的变量,必须要使用
fmt.Println(name)
}
3.定义多个变量
package main
import "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 main
import "fmt"
var (
name string = "os"
userName = "os_lee"
)
func main() {
fmt.Println(name, userName)
}
4.常量定义
定义的时候就要赋值
赋值之后就不能再修改了
package main
import "fmt"
const name string = "os_lee" // 定义就要赋值
func main() {
fmt.Println(name)
}
5.命名规范
核心思想:首字母大写的变量、函数。方法,属性可在包外进行访问
6.输出
package main
import "fmt"
func main() {
fmt.Println("os_lee")
fmt.Println(1)
fmt.Println(true)
fmt.Println("什么", "都", "可以", "输出")
}
格式化输出
%v | 按值的本来值输出 |
%+v | 在 %v 基础上,对结构体字段名和值进行展开 |
%#v | 输出 Go 语言语法格式的值 |
%T | 输出 Go 语言语法格式的类型和值 |
%% | 输出 % 本体 |
%b(oOdxX) | 整型的不同进制方式显示 |
%U | Unicode 字符 |
%s | 字符串 |
%d | 整数 |
%f | 浮点数 |
%p | 指针,十六进制方式显示 |
package main
import "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 main
import "fmt"
func main() {
fmt.Println("输入您的名字:")
var name string
fmt.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=9223372036854775807
fmt.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 main
import "fmt"
func main() {
var c1 = 'a'
var c2 = 97
fmt.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("os\tlee") // 制表符 fmt.Println("os\nlee") // 回车 fmt.Println("\"os\"lee") // 双引号 fmt.Println("os\rlee") // 回到行首 fmt.Println("C:\\pprof\\main.exe") // 反斜杠
其他的转义字符
" | 双引号 |
\ | 反斜线 |
\a | 报警声 |
\b | 退格 |
\f | 换页 |
\n | 换行 |
\r | 回车替换 |
\v | 垂直制表符 |
\t | 制表符 |
多行字符串
在``这个里面,再出现转义字符就会原样输出了
package main
import "fmt"
func main() {
str := `今天
天气
真好
`
fmt.Println(str)
}
5.布尔类型
- 布尔型数据只有 true(真)和 false(假)两个值
- 布尔类型变量的默认值为false
- Go 语言中不允许将整型强制转换为布尔型
- 布尔型无法参与数值运算,也无法与其他类型进行转换
零值问题
如果我们给一个基本数据类型只声明不赋值,那么这个变量的值就是对应类型的零值,例如int就是0,bool就是false,字符串就是""
package main
import "fmt"
func main() {
var a1 int
var a2 float32
var a3 string
var a4 bool
fmt.Printf("%#v\n", a1)
fmt.Printf("%#v\n", a2)
fmt.Printf("%#v\n", a3)
fmt.Printf("%#v\n", a4)
}
四、数组、切片、map
1.数组
数组(Array)是一种非常常见的数据类型,几乎所有的计算机编程语言中都会用到它
- 数组里的元素必须全部为同一类型,要嘛全部是字符串,要嘛全部是整数
- 声明数组时,必须指定其长度或者大小
package main
import "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 main
import "fmt"
func main() {
// 定义一个字符串切片
var list []string
list = 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 main
import "fmt"
func main() {
// 定义一个字符串切片
var list = make([]string, 0)
fmt.Println(list, len(list), cap(list))
fmt.Println(list == nil) // false
list1 := make([]int, 2, 2)
fmt.Println(list1, len(list1), cap(list1))
}
为什么叫切片?
因为切片是数组切出来的
package main
import "fmt"
func main() {
var list = [...]string{"a", "b", "c"}
slices := list[:] // 左一刀,右一刀 变成了切片
fmt.Println(slices)
fmt.Println(list[1:2]) // b
}
切片排序
package main
import (
"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 main
import "fmt"
func main() {
// 声明
var m1 map[string]string
// 初始化1
m1 = make(map[string]string)
// 初始化2
m1 = 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 main
import "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版本可能会根据具体情况采用不同的扩容策略和负载因子阈值。