【教程】快速入门golang
文章目录
- 一、golang简介
- 二、go的基本格式
- 1.源文件基本结构
- 2.核心格式规范
- 3.标识符命名规则
- 三、变量和常量的声明
- 1.变量
- 2.常量
- 四、数据类型
- 1.数字型
- 2.布尔型
- 3.字符串型
- 4.数组
- 5.切片
- 6.map映射
- 五、运算符
- 1.算术运算符
- 2.关系运算符
- 3.逻辑运算符
- 4.按位运算符
- 5.赋值运算符
- 六、控制语句
- 1.条件语句
- 2.循环语句
- 3.循环控制
- 七、函数
- 1.普通函数
- 2.变参函数
- 3.匿名函数
- 4.main和init函数
- 5.空白标识符_
- 6.defer关键字
- 八、异常处理
- 九、时间处理
- 1.格式化时间
- 2.获取当前时间戳
- 3.时间戳和格式化的相互转换
- 4.定时器
- 5.休眠
- 十、指针
- 十一、结构体
- 1.结构体的实例化
- 2.结构体方法
- 3.结构体指针方法
- 4.非结构类型接收器的方法
- 5.匿名结构体
- 6.结构体嵌套(继承)
- 7.结构体和json的转化
- 十二、Golang中的包 go mod
- 1.mod命令
- 2.使用自己创建的包
- 3.使用第三方依赖包
- 4.拿到别人项目的操作
- 十三、接口
- 1.接口的写法
- 2.空接口
- 3.类型断言和类型判断
- 4.指针接收者
- 5.一个结构体实现多个接口
- 6.接口嵌套
- 十四、goroutine channel实现并发和并行
- 1.goroutine 的基本使用
- 2.管道
- 3.单向管道
- 4.select多路复用
- 5.互斥锁
- 6.读写互斥锁
- 十五、反射
- 1.反射基本使用
- 2.反射的常见方法
一、golang简介
2007 年 9 月,**Ken Thompson、Rob Pike、Robert Griesemer 在谷歌内部发起 “20% 时间项目”(谷歌允许员工用 20% 工作时间做个人感兴趣的研发),初衷是解决谷歌当时面临的工程困境 —— 随着代码库扩大,C++ 编译速度越来越慢,Java 内存占用过高,Python 并发性能不足,现有语言难以平衡 “开发效率”“运行效率” 和 “并发能力”。
核心痛点 | 痛点背景 | 解决方案(Go 语言特性) | 效果 / 案例 |
---|---|---|---|
编译慢 | 21 世纪初谷歌 C++ 项目代码量达数千万行,单次编译需数小时,拖累开发效率 | 1. 简化语法,剔除复杂特性;2. 静态链接,规避动态链接开销;3. 单遍编译模式,减少步骤 | 谷歌某 C++ 项目编译耗时从 40 分钟缩至 1 分钟;百万行代码 Go 项目编译在秒级 |
并发复杂 | 硬件进入多核心时代,传统语言通过 “线程 + 锁” 实现并发,代码复杂且易出问题 | 1. Goroutine:轻量级用户级线程,单机器可运行数百万个;2. Channel:基于 CSP 模型,通过消息传递实现安全通信 | Java 实现 1000 个任务并发需手动管理线程池、处理锁竞争;Go 仅需启动 1000 个 Goroutine,通过 Channel 同步,代码简洁低错 |
依赖臃肿 | 早期语言依赖管理存在版本冲突、依赖树臃肿问题,导致大型项目构建失败 | 1. 早期 GOPATH 模式,统一依赖路径;2. 2018 年推出 Go Modules,支持语义化版本、依赖隔离 | 成为现代 Go 项目标准依赖管理方式,大幅降低依赖维护成本,减少构建失败概率 |
开发与运行效率难平衡 | 脚本语言开发快但运行慢、无静态检查;编译型语言运行快但语法繁琐、编译慢 | 1. 静态类型 + 简洁语法,兼顾编译期检查与编码简洁;2. 优化垃圾回收,降低停顿时间;3. 丰富标准库,无需第三方依赖 | 实现 “开发快、运行快、类型安全” 平衡,如无需额外依赖快速搭建高性能 HTTP 服务 |
二、go的基本格式
1.源文件基本结构
一个完整的 Go 源文件通常包含以下部分(按常规顺序排列):
// 1. 包声明(必须放在第一行)
package main // 声明当前文件属于 main 包(可执行程序入口包)// 2. 导入依赖包(多个包可用圆括号批量导入)
import ("fmt" // 标准库的输入输出包"math/rand" // 标准库的随机数包
)// 3. 全局变量声明(可选)
var globalVar int = 100 // 包内可见的全局变量// 4. 函数声明(main 函数是程序入口,仅在 main 包中存在)
func main() {// 函数体:程序执行逻辑fmt.Println("Hello, Go!")
}// 5. 其他自定义函数(可选)
func add(a, b int) int {return a + b
}
2.核心格式规范
-
包声明(Package)
- 每个 Go 文件必须以
package 包名
开头,定义文件所属的包 package main
表示该包是可执行程序,编译后生成可执行文件- 其他包(如
package utils
)通常作为库,供其他包导入使用
- 每个 Go 文件必须以
-
导入语句(Import)
// 单行导入 import "fmt"// 批量导入(推荐) import ("fmt""time" )
- 使用
import
导入所需的包,包名对应标准库或第三方库的路径 - 多个包可使用
()
批量导入,避免重复写import
- 导入未使用的包会导致编译错误(强制代码整洁)
- 使用
-
语句分隔与缩进
// 正确格式 if x > 0 {fmt.Println("x is positive") }// 错误格式({ 单独成行) if x > 0 { // 编译报错fmt.Println("x is positive") }
- Go 语言不需要分号 ; 作为语句结束符(编译器会自动添加)
- 代码块使用
{}
包裹,{
必须与声明语句在同一行,否则编译错误 - 推荐使用 4 个空格 缩进(不强制,但社区通用规范)
-
注释
// 这是单行注释/*这是多行注释第二行内容 */
3.标识符命名规则
- 变量名只能由 字母(a-z, A-Z)、数字(0-9)、下划线(_) 组成,且 不能以数字开头。
- 区分大小写
- 建议使用驼峰命名法,如
userAge
、studentName
- 不能使用关键字
break`、`case`、`chan`、`const`、`continue`、`default`、`defer`、`else`、`fallthrough`、`for`、`func`、`go`、`goto`、`if`、`import`、`interface`、`map`、`package`、`range`、`return`、`select`、`struct`、`switch`、`type`、`var
三、变量和常量的声明
1.变量
在 Go 语言中,变量声明后必须被使用,否则会导致编译错误。这是 Go 语言的一个严格规定,目的是强制开发者编写整洁、无冗余的代码,避免资源浪费和潜在的逻辑错误。
1.1 标准声明(显式指定类型)
- 变量声明以
var
开头,必须指定类型(或通过初始值推断) - 若不指定初始值,会自动赋予「零值」(如
int
为 0,string
为空串""
,bool
为false
)
//语法:var 变量名 类型 = 初始值// 声明单个变量
var age int = 25
var name string = "Alice"
var isStudent bool = true// 声明多个同类型变量(批量声明)
var (a int = 10b int = 20c string = "hello" // 注意:批量声明中不同类型也可共存
)// 不指定初始值(自动赋予零值)
var num int // num = 0
var str string // str = ""
1.2 类型推断(省略类型,由初始值决定)
- 当提供初始值时,Go 会自动推断变量类型,无需显式声明
- 更简洁,是实际开发中最常用的方式之一
//语法:var 变量名 = 初始值var height = 1.85 // 自动推断为 float64 类型
var language = "Go" // 自动推断为 string 类型
var count = 100 // 自动推断为 int 类型
1.3 短变量声明(最简洁的方式,仅限函数内使用)
- 用
:=
替代var
,同时完成「声明 + 赋值」,必须提供初始值 - 编译器会自动推断类型,只能在函数内部使用(全局变量不可用)
- 可同时声明多个变量
// 变量名 := 初始值func main() {// 单个变量score := 95 // 推断为 int 类型// 多个变量x, y := 10, 20 // x=10 (int), y=20 (int)name, age := "Bob", 30 // name="Bob" (string), age=30 (int)
}
注意::= 左侧至少有一个变量是「新声明」的,否则会报错。
a := 10
a := 20 // 错误:a 已声明
a, b := 20, 30 // 正确:b 是新变量
1.4 特殊场景:空白标识符(_
)
- 用于忽略不需要的返回值或变量,避免未使用变量的编译错误
- 不能被读取或赋值,仅作为「占位符」。但是可以多次占位。
// 忽略函数的某个返回值
func getData() (int, string) {return 100, "data"
}func main() {num, _ := getData() // 不能像python一样,必须接收值和函数返回值的数量相等,但是想只取某个值,忽略另一个值,可以用_接收// _ = 20 // 错误:不能给 _ 赋值
}
2.常量
- 常量在声明时必须初始化,且赋值后无法在运行时修改
- 常量的值必须在编译期就能确定,不能依赖运行时计算的结果(如函数返回值)
- 与变量不同,未使用的常量不会导致编译错误,是因为常量在编译期处理,不占用运行时资源
- 常量只能是 Go 中的基本类型:
- 数值类型(
int
、float32
、float64
、complex64
等) - 字符串(
string
) - 布尔值(
bool
)
- 数值类型(
const Pi float64 = 3.1415926 // 单个常量,定义类型
const a = 'hello' // 但个常量,推断类型// 定义多个常量
const(b=1c=2d=3
)// 定义多个常量,以第一行的值为准
const(b1=1c2 //1d2 //1
)
自增常量iota
const (A = iota // A = 0(iota 初始值为 0)B // B = 1(自动递增 1)C // C = 2D // D = 3
)
四、数据类型
go的数据类型分为两类
分类 | 包含类型 | 核心特性(赋值 / 传参时) | 变量独立性 | 默认零值 |
---|---|---|---|---|
值类型 | 基本类型(int/float/bool/string)、数组、结构体 | 拷贝完整底层数据 | 相互独立,修改不影响 | 对应类型零值(0/“” 等) |
引用类型 | 切片、映射、通道、函数、指针 | 仅拷贝引用(指针),共享底层数据 | 共享数据,修改影响所有引用 | nil |
1.数字型
类型类别 | 具体类型 | 取值范围 | 占用字节 | 说明 |
---|---|---|---|---|
整数型(有符号) | int8 | -128 到 127 | 1 | 8 位有符号整数 |
int16 | -32768 到 32767 | 2 | 16 位有符号整数 | |
int32 | -2147483648 到 2147483647 | 4 | 32 位有符号整数(对应 Unicode 码点类型 rune) | |
int64 | -9223372036854775808 到 9223372036854775807 | 8 | 64 位有符号整数 | |
int | 取决于系统架构: - 32 位系统:-2147483648 到 2147483647 - 64 位系统:-9223372036854775808 到 9223372036854775807 | 4 或 8 | 与系统位数一致的有符号整数 | |
整数型(无符号) | uint8 | 0 到 255 | 1 | 8 位无符号整数(对应字节类型 byte) |
uint16 | 0 到 65535 | 2 | 16 位无符号整数 | |
uint32 | 0 到 4294967295 | 4 | 32 位无符号整数 | |
uint64 | 0 到 18446744073709551615 | 8 | 64 位无符号整数 | |
uint | 取决于系统架构: - 32 位系统:0 到 4294967295 - 64 位系统:0 到 18446744073709551615 | 4 或 8 | 与系统位数一致的无符号整数 | |
uintptr | 同 uint 取值范围 | 4 或 8 | 用于指针地址存储,可进行算术运算 | |
浮点型 | float32 | 约 ±1.4e-45 到 ±3.4e38 | 4 | 32 位单精度浮点数,精度约 6 位小数 |
float64 | 约 ±4.9e-324 到 ±1.8e308 | 8 | 64 位双精度浮点数,精度约 15 位小数(默认浮点类型) | |
复数型 | complex64 | 实部和虚部均为 float32 范围 | 8 | 32 位复数(实部 + 虚部各 4 字节) |
complex128 | 实部和虚部均为 float64 范围 | 16 | 64 位复数(实部 + 虚部各 8 字节,默认复数类型) |
常用的方法
功能分类 | 方法 / 函数 | 作用 | 实例 | 输出结果 |
---|---|---|---|---|
类型转换 | T(v) | 将值 v 转换为类型 T | var a int = 10<br>b := float64(a) | b = 10.0 (int→float64) |
strconv.Atoi(s) | 字符串转 int | num, _ := strconv.Atoi("123") | num = 123 | |
strconv.Itoa(n) | int 转字符串 | s := strconv.Itoa(123) | s = "123" | |
strconv.ParseFloat(s, bitSize) | 字符串转 float | f, _ := strconv.ParseFloat("3.14", 64) | f = 3.14 (float64) | |
数学运算 | math.Abs(x) | 取绝对值 | math.Abs(-5.2) | 5.2 |
math.Ceil(x) | 向上取整 | math.Ceil(2.3) | 3.0 | |
math.Floor(x) | 向下取整 | math.Floor(2.7) | 2.0 | |
math.Round(x) | 四舍五入 | math.Round(2.5) | 3.0 | |
math.Pow(x, y) | 计算 x 的 y 次方 | math.Pow(2, 3) | 8.0 | |
math.Sqrt(x) | 计算平方根 | math.Sqrt(16) | 4.0 | |
math.Max(x, y) | 取两数最大值 | math.Max(3, 5) | 5 | |
math.Min(x, y) | 取两数最小值 | math.Min(3, 5) | 3 | |
复数操作 | complex(r, i) | 创建复数 | c := complex(3, 4) | c = 3+4i (complex128) |
real(c) | 取复数实部 | real(3+4i) | 3.0 | |
imag(c) | 取复数虚部 | imag(3+4i) | 4.0 |
注意事项:
- 整数溢出:Go 不会自动处理溢出,需手动判断(如
math.MaxInt32 + 1
会溢出)。 - 浮点精度:float64 精度高于 float32,建议优先使用 float64。
- 性能:数值运算为原生操作,性能接近 C 语言;类型转换和字符串解析有一定开销。
具体类型 | 占用空间(字节) | 取值范围 | 说明 |
---|---|---|---|
整数类型 | |||
int8 | 1 | -128 ~ 127 | 8 位有符号整数 |
int16 | 2 | -32768 ~ 32767 | 16 位有符号整数 |
int32 | 4 | -2147483648 ~ 2147483647(-2³¹ ~ 2³¹-1) | 32 位有符号整数,rune 的底层类型 |
int64 | 8 | -9223372036854775808 ~ 9223372036854775807(-2⁶³ ~ 2⁶³-1) | 64 位有符号整数 |
int | 4 或 8 | 32 位系统:-2147483648 ~ 2147483647;64 位系统:-9223372036854775808 ~ 9223372036854775807 | 随系统位数变化的有符号整数 |
uint8 | 1 | 0 ~ 255 | 8 位无符号整数,byte 的底层类型 |
uint16 | 2 | 0 ~ 65535 | 16 位无符号整数 |
uint32 | 4 | 0 ~ 4294967295(0 ~ 2³²-1) | 32 位无符号整数 |
uint64 | 8 | 0 ~ 18446744073709551615(0 ~ 2⁶⁴-1) | 64 位无符号整数 |
uint | 4 或 8 | 32 位系统:0 ~ 4294967295;64 位系统:0 ~ 18446744073709551615 | 随系统位数变化的无符号整数 |
byte | 1 | 0 ~ 255 | 等价于 uint8,用于表示 ASCII 字符或字节 |
rune | 4 | -2147483648 ~ 2147483647 | 等价于 int32,用于表示 Unicode 字符(支持中文、emoji 等) |
浮点类型 | |||
float32 | 4 | 约 ±1.4e-45 ~ ±3.4e38,精度约 6 位小数 | 32 位单精度浮点数 |
float64 | 8 | 约 ±4.9e-324 ~ ±1.8e308,精度约 15 位小数 | 64 位双精度浮点数(默认浮点类型) |
复数类型 | |||
complex64 | 8 | 实部和虚部均为 float32 范围 | 32 位复数类型 |
complex128 | 16 | 实部和虚部均为 float64 范围 | 64 位复数类型(默认复数类型) |
布尔类型 | |||
bool | 1 | true 或 false(不可用 0/1 替代) | 布尔值类型,默认值为 false |
字符串类型 | |||
string | 动态(引用类型) | 任意长度的字符序列(默认值为空串 “”) | 不可变的字节序列,实际数据存储在堆上,字符串变量存储指针和长度 |
2.布尔型
特性分类 | 具体说明 |
---|---|
类型名称 | bool (全小写,Go 语言关键字,不可自定义同名标识符) |
占用内存 | 1 字节(固定大小,与系统架构无关,确保跨平台一致性) |
合法取值 | 仅两个预定义常量: - true :表示 “真” - false :表示 “假” |
3.字符串型
Go 语言中的 string
类型用于表示 UTF-8 编码的字符序列,是不可变类型(创建后无法直接修改单个字符),广泛用于存储文本数据。
特性分类 | 具体说明 |
---|---|
声明形式 | 1. 双引号 " :用于包含转义字符(如 \n 、\t 、\" ),例:"hello\nworld" 2. 反引号 ```:用于原生字符串(保留换行、空格等格式,不解析转义字符),例:line1\nline2 (输出仍含 \n 字符) |
底层实现 | 基于 []byte (字节切片)存储 UTF-8 编码的字节序列,本质是 “字节数组的只读视图” |
占用内存 | 不固定,取决于字符串长度(每个 UTF-8 字符占 1~4 字节,如英文字母占 1 字节,中文占 3 字节) |
默认零值 | 空字符串 "" (未显式初始化的 string 变量,默认值为长度 0 的空串,非 nil ) |
核心特性 | 1. 不可变性:字符串创建后,无法直接修改单个字符(需通过重新赋值实现 “修改” 效果) 2. UTF-8 编码:原生支持 Unicode 字符,无需额外处理中文、日文等多字节字符 3. 值语义:赋值或传参时会拷贝字符串内容(但因底层优化,短字符串拷贝开销极低) |
常用方法
package mainimport ("fmt""strings""unicode/utf8"
)func main() {// -------------------------------------------------------------长度计算goStr := "Go语言"// 长度计算 len,计算字符串字节(非字符数,UTF-8 需注意)fmt.Println(len(goStr)) // 1(G) + 1(o) + 3(语) + 3(言) = 8// 长度计算utf8.RuneCountInString, 计算字符串的字符数(按 UTF-8 字符统计)fmt.Println(utf8.RuneCountInString(goStr))// ----------------------------------------------------------------子串查找s1 := "abcdefgabcxxxxx1"s2 := "de"s3 := "2222"// 求子串是否存在fmt.Println(strings.Contains(s1, s2)) // truefmt.Println(strings.Contains(s1, s3)) // false// 求子串首次出现的索引,不存在返回-1fmt.Println(strings.Index(s1, s2)) // 3fmt.Println(strings.Index(s1, s3)) // -1// ---------------------------------------------------------修改字符串s := "xxxxx2222SSSS"// 将 `s` 中前 `n` 个 `old` 子串替换为 `new`,`n=-1` 表示替换所有fmt.Println(strings.Replace(s, "x", "aa", 3)) //aaaaaaxx2222SSSS// 大写fmt.Println(strings.ToUpper(s)) //XXXXX2222SSSS// 小写fmt.Println(strings.ToLower(s)) //xxxxx2222ssss// ----------------------------------------------------------分割,拼接aaa := "a,a"bbb := "bbb"// 拼接+fmt.Println(aaa + bbb) //a,abbb// 切割xx := strings.Split(aaa, ",") //类型会变为一个字符串切片fmt.Printf("%v,%T \n", xx, xx) // [a a],[]string// 字符串切片的组合new_aaa := strings.Join(xx, "-")fmt.Println(new_aaa) //a-a// 去首尾空白fmt.Println(strings.TrimSpace(" hello world 1")) //hello world 1
}
4.数组
-
数组的格式:
[长度]元素类型
(如[5]int
表示 “长度为 5、存储 int 类型的数组”),长度是类型的一部分([3]int
和[5]int
是不同类型) -
注意:
- 长度声明后不可变
- 所有元素的类型必须相同,并且在内存中地址连续存储,访问效率高
- 未被初始化的元素赋予零值(如 int 零值为 0,string 零值为 “”)
- 数组是值类型:赋值、传参时会拷贝整个数组(而非引用),修改拷贝后的数组不会影响原数组
- 数组是切片的底层存储(切片本质是数组的 “动态视图”)
-
数组的声明方式
package mainimport "fmt"func main() {// 只声明长度arr1 := [3]int{}fmt.Println(arr1) //[0 0 0]arr2 := [3]string{}fmt.Println(arr2) //[ ]// 省略长度,自动推导arr3 := [...]int{1, 2}fmt.Println(arr3) //[1 2]// 声明长度和指定值arr4 := [2]int{1, 2}arr5 := [5]int{1, 2}fmt.Println(arr4) //[1 2]fmt.Println(arr5) //[1 2 0 0 0]// 指定某个索引是某个值arr6 := [5]int{1: 100, 4: 400}fmt.Println(arr6) //[0 100 0 0 400]}
-
数组的常用方法
package mainimport "fmt"func main() {// 声明空数组var arr [5]int// 获取数组的值arr1 := [5]int{1, 2, 3, 4, 5}fmt.Println(arr1[2]) //3fmt.Println(arr1[2:]) //[3 4 5]//求长度fmt.Println(len(arr1)) //5//数组拷贝arr2 := arr1arr2[1] = 100fmt.Println(arr2) //[1 100 3 4 5]fmt.Printf("%T", arr2) //[5]intfmt.Println(arr1) //[1 2 3 4 5]// 数组循环 12345for i := 0; i < len(arr1); i++ {fmt.Print(arr1[i])}// 数组循环 0 1 1 22 33 44 5for index, value := range arr1 {fmt.Print(index, value)}}
5.切片
切片(Slice)是 Go 语言中最常用的数据结构之一,它建立在数组之上,提供了动态长度的序列视图。与数组的固定长度不同,切片的长度可以动态变化,是处理可变长度数据的理想选择。
-
切片的格式:
[]元素类型
(如[]int
表示 “存储 int 类型的切片”),不包含长度信息(与数组的核心区别) -
组成部分:
- 指针(指向底层数组的起始元素)
- 长度(当前元素数量,
len()
获取)
- 容量(最大可容纳元素数量,
cap()
获取,长度 ≤ 容量。当长度增长超过容量时,会自动扩容(创建新数组,拷贝元素)
-
切片是引用类型:赋值、传参时仅拷贝切片结构(指针、长度、容量),不拷贝底层数组,修改切片会影响共享底层数组的其他切片
-
默认值:
nil
(表示未初始化的切片,len()
和cap()
均为 0) -
声明方法
package mainimport "fmt"func main() {// 声明nil切片var arr []int// 创建切片,基本同数组sliceArr1 := []int{}fmt.Printf("%v,%T\n", sliceArr1, sliceArr1) //[],[]intsliceArr2 := []int{1, 2, 3}fmt.Printf("%v,%T\n", sliceArr2, sliceArr2) //[1 2 3],[]int// 基于数组创建,切片arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}sliceArr3 := arr[1:3]sliceArr4 := arr[1:]fmt.Printf("%v,%T\n", sliceArr3, sliceArr3) //[2 3],[]intfmt.Printf("%v,%T\n", sliceArr4, sliceArr4) //[2 3 4 5 6 7 8 9 10],[]int// make方式创建,明确指定长度和容量(容量可选,默认等于长度)sliceArr5 := make([]int, 5, 5)fmt.Printf("%v,%T\n", sliceArr5, sliceArr5) //[0 0 0 0 0],[]int }
-
常用方法
package mainimport "fmt"func main() {// -----------------------------------------------获取长度,容量sliceArr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}sliceArr0 := sliceArr[2:5]fmt.Println(len(sliceArr0), cap(sliceArr0)) //3,8 8是从原数组索引2后到最后的长度//---------------------------------------向切片末尾添加元素,返回新切片(可能扩容)/*当 append() 导致长度超过容量时,切片会自动扩容:容量 < 1024 时,新容量 = 原容量 × 2容量 ≥ 1024 时,新容量 = 原容量 × 1.25(近似)扩容后会创建新数组,原切片和新切片不再共享底层数组*/sliceArr1 := []int{1, 2, 3, 4}// 单个sliceArr2 := append(sliceArr1, 1)fmt.Println(sliceArr2) //[1 2 3 4 1]// 多个sliceArr3 := append(sliceArr1, 1, 2, 3, 4)fmt.Println(sliceArr3) //[1 2 3 4 1 2 3 4]// -------------------------------------拷贝sliceArr4 := []int{100, 200}sliceArr5 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}min_len := copy(sliceArr5, sliceArr4)fmt.Println(min_len) // 2fmt.Println(sliceArr4) // [100 200]fmt.Println(sliceArr5) // [100 200 3 4 5 6 7 8 9 10]sliceArr6 := []int{100, 200}sliceArr7 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}min_len = copy(sliceArr6, sliceArr7)fmt.Println(min_len) // 2fmt.Println(sliceArr6) // [100 200]fmt.Println(sliceArr7) // [100 200 3 4 5 6 7 8 9 10] }
6.map映射
映射(Map)是一种无序的键值对(key-value)集合,用于快速查找和存储数据。它类似于其他语言中的字典(Dictionary)或哈希表(Hash Table),通过键(key)可以高效地访问、插入和删除对应的值(value)。
-
格式:
map[键类型]值类型
(如map[string]int
表示 “键为 string 类型、值为 int 类型的映射”) -
映射是引用类型:赋值、传参时仅拷贝引用,修改映射会影响所有引用它的变量
-
键要求:
- 键的类型必须是可比较的(支持
==
和!=
运算),如 string、int、bool 等 - 切片、映射、函数等不可比较的类型不能作为键
- 键的类型必须是可比较的(支持
-
值要求:可以是任意类型(包括基本类型、结构体、切片、甚至其他映射)
-
特点:
- 查找、插入、删除操作的平均时间复杂度为 O (1)
- 无需预先定义长度,会动态扩容
- 适合存储键值对形式的数据(如配置信息、缓存等)
-
声明映射
package mainimport "fmt"func main() {// 声明空nil map ,此时映射没有分配内存空间,不能进行添加、修改操作,否则会触发运行时错误:panic: assignment to entry in nil mapvar map1 map[string]intfmt.Println(map1) //map[]// 有值mapmap2 := map[string]int{"a": 1, "b": 2}fmt.Println(map2) //map[a:1 b:2]// make创建,可指定初始容量map3 := make(map[string]int, 10)fmt.Println(map3) //map[]map3["a"] = 1fmt.Println(map3) //map[a:1] }
-
常用方法
package mainimport "fmt"func main() {map1 := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}//获取长度fmt.Println(len(map1)) //4//添加、修改fmt.Println(map1["a"]) //1fmt.Println(map1["xxx"]) //0,不存在为0map1["a"] = 100fmt.Println(map1["a"]) //100// 判断某个值是否存在a, exists := map1["a"]xxx, exists := map1["xxx"]fmt.Println(a, exists) //100 truefmt.Println(xxx, exists) //0 false// 删除delete(map1, "a")fmt.Println(map1["a"]) //0// 循环for k, v := range map1 {fmt.Println(k, v) // 此时的k为键,v为值} }
但是我们实际上经常会需要的是,map的值里包含各种类型,那么怎么解决呢,可以使用空接口实现(具体见下面接口-空接口)
五、运算符
1.算术运算符
运算符 | 名称 | 描述 | 示例 |
---|---|---|---|
+ | 加法 | 将两个操作数相加 | x + y |
- | 减法 | 用第一个操作数减去第二个操作数 | x - y |
* | 乘法 | 将两个操作数相乘 | x * y |
/ | 除法 | 用第一个操作数除以第二个操作数 | x / y |
% | 求模 | 第一个操作数除以第二个操作数后返回余数 | x % y |
2.关系运算符
运算符 | 名称 | 描述 | 示例 | 结果 |
---|---|---|---|---|
== | 等于 | 检查两个操作数是否相等 | 5 == 5 | true |
!= | 不等于 | 检查两个操作数是否不相等 | 5 != 5 | false |
> | 大于 | 检查第一个操作数是否大于第二个操作数 | 6 > 5 | true |
< | 小于 | 检查第一个操作数是否小于第二个操作数 | 6 < 5 | false |
>= | 大于等于 | 检查第一个操作数是否大于或等于第二个操作数 | 5 >= 5 | true |
<= | 小于等于 | 检查第一个操作数是否小于或等于第二个操作数 | 5 <= 5 | true |
3.逻辑运算符
运算符 | 名称 | 描述 | 示例 | 结果 |
---|---|---|---|---|
&& | 逻辑 AND | 两个条件都满足时返回 true | a=true && b=true | true |
|| | 逻辑 OR | 至少一个条件满足时返回 true | a=true || b=false | true |
! | 逻辑非 | 条件不满足时返回 true(取反) | !a(a 为 false 时) | true |
4.按位运算符
运算符 | 名称 | 描述 | 示例(以 8 进制 0b1010 /10 进制 10 、0b0110 /10 进制 6 为例) | 计算过程(二进制) | 结果(十进制) |
---|---|---|---|---|---|
& | 按位与 | 每一位均为 1 时结果为 1,否则为 0 | 10 & 6 | 0b1010 & 0b0110 = 0b0010 | 2 |
| | 按位或 | 任意一位为 1 时结果为 1,否则为 0 | 10 | 6 | `0b1010 | 0b0110 = 0b1110` |
^ | 按位异或 | 两位不同时结果为 1,两位相同时结果为 0 | 10 ^ 6 | 0b1010 ^ 0b0110 = 0b1100 | 12 |
<< | 左移 | 将第一个操作数的位向左移动,移动位数由第二个操作数决定(空位补 0) | 10 << 2 | 0b1010 << 2 = 0b101000 | 40 |
>> | 右移 | 将第一个操作数的位向右移动,移动位数由第二个操作数决定(空位补符号位) | 10 >> 2 | 0b1010 >> 2 = 0b0010 | 2 |
&^ | 按位清除 | 对第二个操作数的位取反后,再与第一个操作数做 “按位与”(清除指定位) | 10 &^ 6 | 0b1010 & ^0b0110 = 0b1010 & 0b1001 = 0b1000 | 8 |
5.赋值运算符
运算符 | 名称 | 描述(等价于) | 示例 | 结果(执行后 x 的值) |
---|---|---|---|---|
= | 简单赋值 | 直接赋值 | x = 5 | 5 |
+= | 加法赋值 | x = x + y | x = 3; x += 2 | 5 |
-= | 减法赋值 | x = x - y | x = 5; x -= 3 | 2 |
*= | 乘法赋值 | x = x * y | x = 2; x *= 4 | 8 |
/= | 除法赋值 | x = x / y | x = 8; x /= 2 | 4 |
%= | 模赋值 | x = x % y | x = 7; x %= 3 | 1 |
&= | 按位与赋值 | x = x & y | x = 6 (0b110); x &= 3 (0b011) | 2 (0b010) |
^= | 按位异或赋值 | x = x ^ y | x = 6 (0b110); x ^= 3 (0b011) | 5 (0b101) |
= | 按位或赋值 | x = x | y | x = 6 (0b110); x |= 3 (0b011) | 7 (0b111) |
六、控制语句
1.条件语句
if语句
// ----------------if语句
if(condition) {//if要执行的语句//condition为真
}// --------------if-elseif (condition) {// if条件为true,执行此代码块} else {// if条件为false,执行此代码块
}//-----------------嵌套if
if (condition1) {// 当条件1为真时执行if (condition2) {// 当条件2为真时执行}//--------------------------if..else..if
if(condition_1) {//condition_1为true,执行这里的代码块
} else if(condition_2) {//condition_2为true,执行这里的代码块
}else {//没有条件为true时,执行这里代码块
}
switch语句
- 表达式switch中的optstatement和optexpression都是可选语句。
- 如果同时存在optstatement和optpression,则在它们之间需要使用分号(;)。
- 如果switch不包含任何表达式,则编译器会假定该表达式为true。
switch optstatement(可选); optexpression(可选){case expression1: Statement..case expression2: Statement.....default(可选): Statement..
}
2.循环语句
1.普通循环
- 在*初始化(initialization)*语句是可选的,用于循环开始之前执行。初始化语句始终位于简单的语句中,例如变量声明,递增或赋值语句或函数调用。
- 条件(condition)语句包含一个布尔表达式,该表达式在循环的每次迭代开始时计算。如果条件语句的值为true,则执行循环。
- post语句在for循环体之后执行。在post语句之后,条件语句再次计算条件语句的值是否为false,然后循环结束。
for initialization; condition; post{// 语句....
}
2.死循环
for{// 语句...
}
3.用做while循环
for condition{//语句..
}//例如
i:= 0
for i < 3 { i++
}
4.range循环
- j可选
- i一般为索引,在映射中为键
for i, j:= range rvariable{// 语句..
}
3.循环控制
1.break
循环停止
2.continue
结束本次循环
3.goto
该语句用于将控制转移到程序中的标记语句。标签是有效的标识符,放在控件转移处的语句前面。由于难以跟踪程序的控制流,程序员通常不使用goto语句。
package main import "fmt"func main() { var x int = 0 //for循环的工作原理与while循环相同Lable1: for x < 8 { if x == 5 { //使用goto语句x = x + 1; goto Lable1 } fmt.Printf("值为: %d\n", x); x++; }
}
七、函数
1.普通函数
package mainimport "fmt"func test() {fmt.Println("无形参,无返回值,可以不用写参数列表和返回值类型")
}func test2() int {fmt.Println("无形参,有1个返回值,可以不用写参数列表型")return 1
}func test3() (int, string) {fmt.Println("无形参,有2个返回值,可以不用写参数列表,接收返回值必须用和返回值类型相等数量的接收")return 1, "test3"
}func test4(a int, b string) (int, string) {// 参数可以合并为 a,b intfmt.Println("有形参,有2个返回值,接收返回值必须用和返回值类型相等数量的接收")return a, b
}func test5(a int, b string) (int, string) {// 参数可以合并为 a,b intfmt.Println("有形参,有2个返回值,接收返回值必须用和返回值类型相等数量的接收")return a, b
}func main() {test() //无形参,无返回值,可以不用写参数列表和返回值类型test2() // 无形参,有1个返回值,可以不用写参数列表型a, b := test3() //无形参,有2个返回值,可以不用写参数列表,接收返回值必须用和返回值类型相等数量的接收fmt.Println(a, b) //1 test3a, b = test4(1, "test4") //有形参,有2个返回值,接收返回值必须用和返回值类型相等数量的接收fmt.Println(a, b) //1 test4}
2.变参函数
- 当要在函数中传递切片时,使用可变参数函数。
- 不知道参数的数量时,使用可变参数函数。
package main
import "fmt"func test(a int, b ...string) {fmt.Println(a, b)
}func main() {a := 100b := "b"c := "c"d := "d"test(a, b, c, d) //100 [b c d]
}
3.匿名函数
无名称的函数,也叫函数字面量
package mainimport "fmt"func main() {func() {fmt.Println("hello world")}()// 变量接收函数a := func() {fmt.Println("你好")}a()// 变量接收函数,有参数b := func(x int) int {return x}b(5)
}
4.main和init函数
- main包是一个特殊的软件包,与可执行程序一起使用,并且该package包含*main()函数。在main()函数是一种特殊类型的函数,它是可执行程序的入口点。它不带任何参数也不返回任何内容。由于可以自动调用main()函数,因此无需显式调用main()函数,并且每个可执行程序必须包含一个package main和main()*函数。
- init()函数就像main函数一样,不带任何参数也不返回任何东西。
- 每个包中都存在此函数,并且在初始化包时将调用此函数。
- 该函数是隐式声明的,不能从任何地方引用它,并且可以在同一程序中创建多个init()函数,并且它们将按照创建顺序执行。
- init()函数的主要目的是初始化无法在全局上下文中初始化的全局变量。
5.空白标识符_
因为go规定了值必须要被使用,但是有的函数返回多个值我们却用不到,可以使用_
package mainimport "fmt"func test() (int, int) {a, b := 1, 2return a, b
}func main() {x, _ := test()fmt.Println(x)_, y := test()fmt.Println(y)
}
6.defer关键字
defer的作用是延迟函数的执行
- 在Go语言中,同一程序中允许多个defer语句,并且它们按后进先出执行。
- 在defer语句中,将在执行defer语句时(而不是在调用它们时)评估参数。
- defer语句通常用于确保在完成文件处理后关闭文件,关闭通道或捕获程序中的紧急情况。
package mainimport "fmt"func test1() {fmt.Println(1111)
}
func test2() {fmt.Println(2222)
}
func test3() {fmt.Println(3333)
}
func test4() {fmt.Println(4444)
}
func test5() {fmt.Println(5555)
}
func main() {defer test1()defer test2()defer test3()test4()defer test5()//4444//5555//3333//2222//1111}
八、异常处理
在 Go 语言中,并没有像 Java、Python 等语言中的 try/catch 异常处理机制,而是采用了一种不同的错误处理方式 —— 通过函数返回值来传递错误信息。这种设计理念强调显式处理错误,让开发者更关注错误的处理逻辑。
package mainimport ("fmt"
)// 买水函数:返回水和可能的错误
func 买水(钱 int) (水 string, 错误信息 error) {if 钱 < 2 {// 钱不够,返回错误信息return "", fmt.Errorf("钱不够,还差 %d 元", 2-钱)}// 成功买到水return "一瓶矿泉水", nil // nil 表示没有错误
}func main() {result, err := 买水(1)if err != nil {fmt.Println(err)} else {fmt.Println(result)}
}
九、时间处理
1.格式化时间
package mainimport ("fmt""time"
)func main() {//获取当前时间current_time := time.Now()fmt.Println(current_time) //2025-09-04 13:33:54.6276099 +0800 CST m=+0.000000001// 格式化时间为想要的格式,go的诞生时间为2006年1月2号15点04分// 24小时 ,时间为15f_time := current_time.Format("2006-01-02 15:04:05")fmt.Println(f_time) //2025-09-04 13:36:34// 12小时 ,时间为03f_time = current_time.Format("2006-01-02 03:04:05")fmt.Println(f_time) //2025-09-04 01:36:34}
2.获取当前时间戳
package mainimport ("fmt""time"
)func main() {//获取当前时间current_time := time.Now()fmt.Println(current_time) //2025-09-04 13:33:54.6276099 +0800 CST m=+0.000000001timestamp := current_time.Unix() //毫秒fmt.Println(timestamp) //1756964364
}
3.时间戳和格式化的相互转换
package mainimport ("fmt""time"
)func main() {// 时间戳转时间格式timestamp := 1756964364timeObj := time.Unix(int64(timestamp), 0) // 注意类型需要为int64f_time := timeObj.Format("2006-01-02 15:04:05")fmt.Println(f_time) // 2025-09-04 13:39:24// 时间格式转时间戳f_time2 := "2025-09-04 13:39:24"tp, _ := time.ParseInLocation("2006-01-02 15:04:05", f_time2, time.Local)fmt.Println(tp.Unix())
}
4.定时器
package mainimport ("fmt""time"
)func main() {i := 0tricker := time.NewTicker(time.Second)for t := range tricker.C {fmt.Println(t)i++if i == 5 {tricker.Stop() //销毁break}}
}//2025-09-04 13:52:52.0289261 +0800 CST m=+1.000000001
//2025-09-04 13:52:53.0289261 +0800 CST m=+2.000000001
//2025-09-04 13:52:54.0289261 +0800 CST m=+3.000000001
//2025-09-04 13:52:55.0289261 +0800 CST m=+4.000000001
//2025-09-04 13:52:56.0289261 +0800 CST m=+5.000000001
5.休眠
time.Sleep(2 * time.Second) //休眠2stime.Nanosecond(1 纳秒)
time.Microsecond(1 微秒 = 1000 纳秒)
time.Millisecond(1 毫秒 = 1000 微秒)
time.Second(1 秒 = 1000 毫秒)
time.Minute(1 分钟 = 60 秒)
time.Hour(1 小时 = 60 分钟)
十、指针
指针是一个变量,用于存储另一个变量的内存地址。Golang中的指针也称为特殊变量。始终以十六进制格式找到内存地址(以0x开头,如0xFFAAF等)
a := 10
p := &a // p就是指针变量,类型为*int
fmt.Printf("%p \n", &a) //0xc00000a088 a的内存地址
fmt.Printf("%p \n", p) //0xc00000a088 ,此时p存的值为a的地址
fmt.Printf("%T \n", p) //*int
fmt.Println(&p) //0xc00006c040,此时为p的地址
fmt.Println(*p) // 10 ,通过*取到a的值// 改变对应地址的值
*p = 20
fmt.Println(a) //20
初始化指针
// 空指针是引用类型,需要分配内存空间,所以报错
//var a *int //nil
//*a = 10
//fmt.Println(a)// new函数创建,空指针
a := new(int)
*a = 100
fmt.Println(a) //0xc00000a088
fmt.Println(*a) //100
fmt.Println(&a) //0xc00006c038 a本身也是一个指针变量,也含有自己的内存空间
new和make的区别
- 二者都是用来做内存分配的。
- make 只用于 slice、map 以及 channel 的初始化,返回的还是这三个引用类型本身
- 而new 用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针
&和*的区别
运算符 | 名称 | 作用 | 使用场景示例 |
---|---|---|---|
& | 取地址符 | 获取变量的内存地址(返回指针类型) | &a 代表a的内存地址 |
* | 解引用指针 | 通过指针访问其指向的变量的值 | *a 代表a指向的值 |
* | 指针类型声明符 | 声明一个指针类型(表示 “指向某种类型的指针”) | a *int 声明一个指针 |
十一、结构体
golang中没有类的概念。Golang中的结构(struct)是一种用户定义的类型,允许将可能不同类型的项分组/组合成单个类型
1.结构体的实例化
定义结构体并实例化
package mainimport "fmt"type Person struct {name stringage intsex string
}func main() {// 实例化Personvar p1 Personp1.name = "张三"p1.age = 20p1.sex = "沃尔玛塑料袋"fmt.Println(p1) //{张三 20 沃尔玛塑料袋}// 实例化Person,按顺序赋值var p2 = Person{"李四", 15, "男"}fmt.Println(p2) //{李四 15 男}// 实例化Personvar p4 = Person{name: "二狗子",age: 20,sex: "女", //结尾必须加,}fmt.Println(p4) //{二狗子 20 女}
}
结构体指针
package main
import "fmt"type Person struct {name stringage intsex string
}func main() {// new方式实例结构体, 此时为结构体指针var p3 = new(Person)p3.sex = "女"p3.name = "彪子"p3.age = 12fmt.Println(p3) //&{彪子 12 女} 此时为结构体指针p3.age = 15fmt.Println(p3.age) // 15(*p3).age = 22 // 结构体指针可以直接改值,也可以按照原来的指针方式改值fmt.Println(p3.age) //22
}
注意事项
- 结构体名字首字母大写为公有结构体,小写为私有结构体。公有的可跨包使用,私有的只能本包使用
- 注意结构体指针可以按照原来的指针方式改值,也可以直接改值
2.结构体方法
package mainimport "fmt"type Person struct {name stringage intsex string
}// 和函数类似,前边包含一个接收者参数。接收者和接收者类型必须出现在同一个包中。
func (p Person) PrintInfo() {fmt.Println(p.name)
}func main() {// 实例化Personvar p4 = Person{name: "二狗子",age: 20,sex: "女", //结尾必须加,}p4.PrintInfo()
}
3.结构体指针方法
package mainimport "fmt"// Author 结构体
type author struct {name stringbranch string
}// 带有指针的方法
// author类型的接收者
func (a *author) show_1(abranch string) {(*a).branch = abranch
}// 带有值的方法
// 作者类型的接收者
func (a author) show_2() {a.name = "Gourav"fmt.Println("Author's name(Before) : ", a.name)
}func main() {//初始化作者结构体res := author{name: "Sona",branch: "CSE",}fmt.Println("Branch Name(Before): ", res.branch) // CSE//调用show_1方法//(指针方法)带有值res.show_1("ECE")fmt.Println("Branch Name(After): ", res.branch) //ECE//调用show_2方法//带有指针的(值方法)(&res).show_2() ////Gourav fmt.Println("Author's name(After): ", res.name) // Sona
}
4.非结构类型接收器的方法
在Go语言中,只要类型和方法定义存在于同一包中,就可以使用非结构类型接收器创建方法。如果它们存在于int,string等不同的包中,则编译器将抛出错误,因为它们是在不同的包中定义的。
package mainimport "fmt"type data int // 类型定义// 非结构类型的接收器
func (d1 data) multiply(d2 data) data {return d1 * d2
}func main() {value1 := data(2)value2 := data(5)res := value1.multiply(value2)fmt.Println("最终结果: ", res) //10
}
5.匿名结构体
匿名结构是不包含名称的结构。
package mainimport "fmt"func main() {Element := struct {name stringage int}{name: "詹三",age: 20,}fmt.Println(Element) //{詹三 20}
}
在Go结构中,允许创建匿名字段。匿名字段是那些不包含任何名称的字段,你只需要提到字段的类型,然后Go就会自动使用该类型作为字段的名称。
-
在结构中,不允许创建两个或多个相同类型的字段
type student struct {intint }
-
允许匿名字段和命名字段同时存在
package mainimport "fmt"type student struct {name stringage intint //相当于隐士创建了int名字 }func main() {s := student{name: "詹三",age: 20,int: 15,}fmt.Println(s) //{詹三 20 15} }
以下是一个正确的匿名例子
package mainimport "fmt"type student struct {stringint }func main() {s := student{string: "詹三",int: 15,}fmt.Println(s) //{詹三 15} }
6.结构体嵌套(继承)
一个结构体(子结构体)直接 “复用” 另一个结构体(父结构体)的字段和方法,同时支持扩展自身的字段和方法。
普通嵌套
package main
import "fmt"type Author struct {name stringbranch stringyear int
}type HR struct {details Author // 结构体嵌套,也是继承
}func main() {result := HR{details: Author{"Sona", "ECE", 2013},}fmt.Println(result) //{{Sona ECE 2013}}
}
继承父结构体方法
- 若子结构体定义了与父结构体同名、同参数、同返回值的方法,会 “覆盖” 父结构体的方法(即调用时优先执行子结构体的方法),实现类似 “方法重写” 的效果。
- 子结构体可同时匿名嵌套多个父结构体,从而复用多个 “父结构体” 的字段和方法(类似多继承)。但需注意:若多个父结构体有同名字段或方法,直接访问会编译报错,必须显式指定父结构体来源。
package mainimport "fmt"// 父结构体:Person
type Person struct {Name string
}// 父结构体的方法:SayHello
func (p *Person) SayHello() {fmt.Printf("大家好,我是 %s\n", p.Name)
}// 子结构体:Student(匿名嵌套 Person)
type Student struct {Person Score int
}func main() {s := Student{Person: Person{Name: "小红"},Score: 90,}// 子结构体实例直接调用父结构体的方法s.SayHello() // 输出:大家好,我是小红
}
Go 组合 vs 传统继承的核心区别
对比维度 | 传统继承(如 Java) | Go 结构体匿名嵌套(组合) |
---|---|---|
实现方式 | 通过 extends 关键字,子类是父类的 “特殊类型” | 通过匿名嵌套,子结构体 “包含” 父结构体 |
耦合性 | 强耦合:子类依赖父类的实现,父类修改可能影响子类 | 低耦合:子结构体仅复用父结构体的接口,父类内部修改不影响子类 |
多继承支持 | 不一定(如 Java 仅支持单继承,通过接口间接实现) | 支持(可嵌套多个父结构体),但需处理字段 / 方法冲突 |
灵活性 | 固定层级关系,扩展困难 | 可灵活组合多个结构体,按需复用 |
7.结构体和json的转化
私有属性(首字母小写的)不能被json包访问
package mainimport ("encoding/json""fmt"
)type Student struct {ID intName stringage int // 私有变量
}func main() {s := Student{ID: 1,Name: "张三",age: 20,}//结构体实例转为jsonjsonBytes, _ := json.Marshal(s) //此时jsonBytes是一个byte切片jsonString := string(jsonBytes)fmt.Println(jsonString) //{"ID":1,"Name":"张三"}//json字符串转换为结构体,需要先定义结构体,只返回结构体已给的内容,如果json中与定义的字段类型不一样,会报错var sss = `{"ID":1,"Name":"张三","age":"20","Sex":"男"}`var ssss Studenterr := json.Unmarshal([]byte(sss), &ssss) //此处为指针变量if err != nil {fmt.Println(err)}fmt.Println(ssss) //{1 张三 0}
}
结构体标签
package mainimport ("encoding/json""fmt"
)type Student struct {ID int `json:"id"` // 结构体标签Name string `json:"Name"` // 结构体标签
}func main() {s := Student{ID: 1,Name: "张三",}//结构体实例转为jsonjsonBytes, _ := json.Marshal(s) //此时jsonBytes是一个byte切片jsonString := string(jsonBytes)fmt.Println(jsonString) //{"id":1,"Name":"张三"}
}
十二、Golang中的包 go mod
1.mod命令
命令格式 | 功能描述 | 示例 |
---|---|---|
go mod init <模块路径> | 初始化新模块,生成 go.mod 文件 | go mod init github.com/user/project |
go mod tidy | 自动添加缺失的依赖,移除未使用的依赖,更新 go.mod 和 go.sum | - |
go get <依赖路径>@<版本> | 添加或更新指定依赖(版本可指定标签、分支或 Commit 哈希) | go get github.com/gin-gonic/gin@v1.9.1 |
go get -u <依赖路径> | 将指定依赖更新到最新次要版本或补丁版本 | go get -u github.com/gin-gonic/gin |
go get <依赖路径>@none | 从当前模块中移除指定依赖 | go get github.com/old/dep@none |
go mod graph | 以图形化方式展示模块的依赖关系 | - |
go mod download | 下载 go.mod 中列出的所有依赖到本地缓存 | - |
go mod vendor | 将依赖复制到项目的 vendor 目录(用于离线开发) | - |
go mod verify | 验证依赖是否与 go.sum 中记录的校验和一致,确保依赖未被篡改 | - |
go mod edit <选项> | 编辑 go.mod 文件(如修改模块路径、添加 / 替换依赖等) | go mod edit -module github.com/new/name |
go list -m all | 列出当前模块及其所有依赖的信息 | - |
go list -m <依赖路径> | 查看指定依赖的详细信息(版本、路径等) | go list -m github.com/gin-gonic/gin |
2.使用自己创建的包
初始化项目,会生成一个mod文件
go mod init 项目名
新建一个文件夹,定义一个
导包执行
给包起别名
package mainimport H "test_demo/haha"func main() {H.Name()
}
注意事项:
- 一个文件夹下可以有多个go文件,上图的话就是haha文件夹可以有多个go文件
- 包名可以不和文件夹名一样,包名不能包含-符号
- 一个文件夹下面包含的文件只能归属一个package,同一个package的文件不能在多个文件夹下
- 一个包不同文件的方法名 不能重复
3.使用第三方依赖包
package mainimport ("fmt""github.com/shopspring/decimal" // 第三方依赖
)func main() {price, err := decimal.NewFromString("100.005")fmt.Println(price, err)
}
此时包不存在,执行此命令
go mod tidy
然后可执行,,输出结果
100.005 <nil>
4.拿到别人项目的操作
当拿到一个项目的时候,可以把下面两个文件删除
go.mod
go.sum
然后执行这两个补足环境
go mod init 项目名
go mid tidy
十三、接口
1.接口的写法
接口是一种数据类型,实际上就是一个行为规范,只定义规范不识闲功能。只需要一个变量含有接口类型的所有方法,那么这个变量就实现了这个接口。
注意:接口里有方法的话,必须要通过结构体或者自定义类型实现这个接口。
定义接口
package mainimport ("fmt"
)type myinterface interface {// 方法fun1() intfun2() string
}
type Student struct{}func (stu Student) fun1() int {return 111
}
func (stu Student) fun2() string {return "aaaa"
}func (stu Student) fun3() string {return "fun3"
}func main() {stu := Student{}var my myinterface = stu //把结构体实例赋值给接口的my实例,表示结构体实例实现了my这个接口fmt.Println(my.fun1()) //111fmt.Println(my.fun2()) //aaaa//fmt.Println(my.fun3()) //错误!该接口没有此方法,无法调用fmt.Println(stu.fun3()) //但是结构体实例是可以正常调用的
}
2.空接口
Golang中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。
空接口可以直接当类型使用,表示任意类型
package mainimport "fmt"type A interface{} //空接口func main() {var a A// var a interface{} //也可以这样定义一个变量为空接口a = "你好"fmt.Printf("%v %T \n", a, a) //你好 stringa = 100fmt.Printf("%v %T \n", a, a) //100 int
}
也可以这么写
package mainimport ("fmt"
)func fun1(a interface{}) {fmt.Printf("type:%T,value:%v\n", a, a)
}func main() {fun1(100) //type:int,value:100fun1("你好") //type:string,value:你好fun1([]int{1, 2, 3}) //type:[]int,value:[1 2 3]fun1(map[int]int{1: 1, 2: 2}) //type:map[int]int,value:map[1:1 2:2]
}
对于映射或者切片,以前的值只能固定一种类型,现在有了接口,可以写各种类型
package mainimport ("fmt"
)func main() {// 可以放任何值的映射var m1 = make(map[string]interface{})m1["name"] = "张三"m1["age"] = 23m1["is_girl"] = truefmt.Println(m1) //map[age:23 is_girl:true name:张三]// 可以放任何值的切片var s1 = []interface{}{1, 2, "李四", true}fmt.Println(s1) //[1 2 李四 true]
}
注意运用空接口时候,不能直接获取切片或者映射的具体属性,可以使用下边的断言
package mainimport "fmt"func main() {var m1 = make(map[string]interface{})m1["name"] = "张三"m1["score"] = []int{90, 95, 100}fmt.Println(m1) //map[name:张三 score:[90 95 100]]fmt.Println(m1["score"]) //[90 95 100]//fmt.Println(m1["score"][0]) // 接口类型,无法根据索引取值,可以使用断言// 将 interface{} 转换为 []intif scores, ok := m1["score"].([]int); ok {fmt.Println(scores[0]) // 90} else {fmt.Println("无法将 score 转换为 []int 类型")}
}
3.类型断言和类型判断
一个接口的值是由一个具体类型+具体类型的值两个部分组成。这2和部分分别叫动态值和动态类型。
如果想判断空接口值的类型,就要使用类型断言.
x.(T)x:表示类型为interface{}的变量
T:表示断言x可能是的类型
例子:
package mainimport ("fmt"
)func main() {var a interface{}//a = "hello world"a = 111v, ok := a.(string) //判断是不是string,第一个为值,第二个为判断的bool值if ok {fmt.Println(v)fmt.Printf("string类型")} else {fmt.Printf("不是string类型,断言失败")}
}
**类型判断:**在Go接口中,类型判断用于将接口的具体类型与case语句中提供的多种类型进行比较。
package mainimport ("fmt"
)func myfun(a interface{}) {//使用类型判断,a.(type) 可以直接判断类型 ,但是只能用在Switch语句里面switch a.(type) {case int:fmt.Println("类型: int,值:", a.(int))case string:fmt.Println("\n类型: string,值: ", a.(string))case float64:fmt.Println("\n类型: float64,值: ", a.(float64))default:fmt.Println("\n类型未找到")}
}
func main() {myfun(111) //类型: int,值: 111myfun("111") //类型: string,值: 111myfun([]int{1, 2, 3}) //类型未找到
}
4.指针接收者
结构体实现接口的方法时,接收者值可以为值类型也可以为指针类型。
- 值接收者:结构体值接收者实例化后的结构体值类型和结构体指针类型都可以赋值给接口变量
- 指针接收者:如果结构体中的方法是指针接收者,那么实例化后结构体指针类型可以复制给接口变量,结构体值类型没法复制给接口变量
①如果是值类型接收者,加不加&都可以实现
package mainimport ("fmt"
)type Usber interface {start()stop()
}type Phone struct {Name string
}// 值接收者
func (p Phone) start() {fmt.Print("start")
}func (p Phone) stop() {fmt.Print("stop")
}func main() {var p1 = Phone{Name: "小米",}var u1 Usber = p1u1.start() //startvar p2 = &Phone{Name: "小米",}var u2 Usber = p2u2.start() //start}
②如果是指针类型接收者,必须加&
package mainimport ("fmt"
)type Usber interface {start()stop()
}type Phone struct {Name string
}// 指针接收者
func (p *Phone) start() {fmt.Print("start")
}func (p *Phone) stop() {fmt.Print("stop")
}func main() {var p2 = &Phone{Name: "小米",}var u2 Usber = p2u2.start() //start}
5.一个结构体实现多个接口
package mainimport "fmt"type Animal interface {SetName(string)
}
type Animal2 interface {GetName() string
}type Dog struct {Name string
}func (d *Dog) SetName(name string) {d.Name = name
}
func (d *Dog) GetName() string {return d.Name
}func main() {d := &Dog{Name: "小黑",}var d1 Animal = dvar d2 Animal2 = dd1.SetName("小白")fmt.Println(d2.GetName())
}
6.接口嵌套
package mainimport "fmt"type A interface {SetName(string)
}
type B interface {GetName() string
}
// 接口嵌套
type Animaler interface {AB
}
type Dog struct {Name string
}func (d *Dog) SetName(name string) {d.Name = name
}
func (d *Dog) GetName() string {return d.Name
}func main() {d := &Dog{Name: "小黑",}var d1 Animaler = dd1.SetName("小白")fmt.Println(d1.GetName())
}
十四、goroutine channel实现并发和并行
1.goroutine 的基本使用
并发和并行都是针对线程的。多线程程序在单核CPU上面运行就是并发,多线程程序在多核CPU上运行就是并行。如果线程数大于CPU核数,则多线程程序在多个CPU上既有并发又有并行。
- 并发:多个线程同时竞争一个位置,竞争到的才可以执行,每个时间段中只有一个线程在执行
- 并行:多个线程同时执行,每个时间段,可以有多个线程同时执行
- 协程: 可以理解为用户级线程,系统不知道有协程的存在,完全由用户自己程序调度的。
- golang中的主线程:可以理解为线程也可以理解为进程,在一个go程序的主线程上可以起多个协程(goroutine),多协程可以实现并发或者并行。go语言从语言层面原生支持协程。
看下这个例子
package mainimport ("fmt""time"
)func a() {for i := 0; i < 10; i++ {fmt.Printf("a:%d\n ", i)time.Sleep(1 * time.Second)}
}func b() {for i := 0; i < 10; i++ {fmt.Printf("a:%d\n ", i)time.Sleep(1 * time.Second)}
}func main() {go a() //本来是按顺序执行,加了go,可以一起执行b()
}/*
b:0a:0a:1b:1a:2b:2b:3a:3b:4a:4a:5b:5b:6a:6a:7b:7b:8a:8a:9b:9
*/
但是主线程执行完毕后,不管协程执行完毕没,都会结束,自动退出。比如我们把b方法改为5次
func b() {for i := 0; i < 5; i++ {fmt.Printf("a:%d\n ", i)time.Sleep(1 * time.Second)}
}/*
b:0a:0a:1b:1b:2a:2b:3a:3a:4b:4
*/
这里我们使用sync.WaitGroup解决
package mainimport ("fmt""sync""time"
)var wg sync.WaitGroup //声明一个该类型的变量func a() {for i := 0; i < 10; i++ {fmt.Printf("a:%d\n ", i)time.Sleep(1 * time.Second)}wg.Done() //协程计数器-1
}
func b() {for i := 0; i < 10; i++ {fmt.Printf("b:%d\n ", i)time.Sleep(1 * time.Second)}wg.Done() //协程计数器-1
}func main() {wg.Add(1) //协程计数器+1go a()wg.Add(1) //协程计数器+1go b()wg.Wait()
}/*
a:0
b:0
b:1
a:1
a:2
b:2
a:3
b:3
a:4
b:4
a:5
a:6
a:7
a:8
a:9
*/
2.管道
管道类似其他语言的队列,遵循先进先出规则。管道channel可以在多个goroutine中传递信息。channel是一种类型,一种引用类型
var 变量 chan 元素类型// 例子
var ch1 chan int //整数型管道
var ch2 chan bool //bool型管道
var ch3 chan []int //切片管道
也可以使用make初始化容量,方才能使用
make(chan 元素类型,容量)
例如:
// 创建
var ch=make(chan int,3)//1.发送(数据推入管道)
ch <- 10
ch <- 20//2.接收(管道内取值)
data := <-ch//3.关闭管道
close(ch)
造成管道阻塞(deadlock)的几种方式:
- 当推的数量超过容量,就会造成管道阻塞
- 当管道没值了还再取值,也会造成管道阻塞
循环取管道的值:
package mainimport "fmt"func main() {var ch1 = make(chan int, 10)for i := 0; i < 10; i++ {ch1 <- i}close(ch1)// for range遍历管道,没有key和索引。写完以后必须关闭,否则会死锁 fatal error: all goroutines are asleep - deadlock!for val := range ch1 {fmt.Println(val)}// 普通for,执行此处需要注释close(ch1),此处不需要关闭for i := 0; i < 10; i++ {fmt.Println(<-ch1)}}
在 Go 中,管道(channel)关闭后有以下特性:
- 不能再向关闭的管道写入数据,否则会触发 panic(运行时错误)。
- 可以从关闭的管道中读取数据,直到管道中的数据被取完。
- 当管道中有数据时,读取操作会正常返回数据和
true
(表示读取成功)。- 当管道中的数据被取完后,读取操作会返回该管道元素类型的零值和
false
(表示管道已关闭且无数据)。
3.单向管道
像刚才创建管道可读可写,但是也可以创建单向的
双向管道
make(chan int,10)只写管道,取值会报错
make(chan<- int,10)只读管道,只能取值
make(<-chan int,10)
这个的用途,比如我们创建了个函数,这个函数参数的管道,我只允许你写数据或者读数据,就可以限制了
package mainimport "fmt"func fun1(ch chan<- int) {ch <- 100fmt.Println("100")
}func main() {var ch1 = make(chan int, 10)fun1(ch1)
}
4.select多路复用
某些场景下需要从多个通道接收数据,可以用select多路复用
package mainimport "fmt"func main() {intChan := make(chan int, 10)for i := 1; i <= 10; i++ {intChan <- i}strChan := make(chan string, 10)for i := 1; i <= 10; i++ {strChan <- fmt.Sprintf("---%d", i)}for {//随机取一个通道的值。此处不需要关闭channelselect {case i := <-intChan:fmt.Println(i)case s := <-strChan:fmt.Println(s)default:fmt.Println("所有通道都没值了")return //记住退出}}}
/*
1
---1
2
---2
---3
3
---4
---5
---6
---7
---8
---9
4
---10
5
6
7
8
9
10
所有通道都没值了
*/
5.互斥锁
在 Go 语言中,互斥锁(sync.Mutex
)是解决并发安全问题的常用机制,用于保证同一时间只有一个 goroutine 能访问共享资源,防止数据竞争。
sync.Mutex
提供了两个核心方法:
Lock()
:获取锁,如果锁已被其他 goroutine 获取,则阻塞等待Unlock()
:释放锁,必须在Lock()
之后调用,否则会导致 panic
var mutex sync.Mutex
加锁例子
package mainimport ("fmt""sync"
)var (count int // 共享变量mu sync.Mutex // 互斥锁wg sync.WaitGroup // 用于等待所有goroutine完成
)// 对共享变量进行累加操作
func increment() {defer wg.Done()// 加锁:保证同一时间只有一个goroutine执行后续操作mu.Lock()count++// 解锁:在函数退出前释放锁,避免死锁defer mu.Unlock()
}func main() {// 启动100个goroutine同时操作countfor i := 0; i < 100; i++ {wg.Add(1)go increment()}wg.Wait() // 等待所有goroutine完成fmt.Println("最终结果:", count) // 正确输出 100
}
关键点说明:
- 死锁风险:
- 如果获取锁后忘记释放(未调用
Unlock()
),会导致其他 goroutine 永远阻塞 - 推荐使用
defer mu.Unlock()
确保锁一定会被释放
- 如果获取锁后忘记释放(未调用
- 锁的粒度:
- 锁的范围应尽可能小(只保护必要的代码块),减少并发阻塞时间
- 避免在持有锁时进行耗时操作(如 IO、网络请求)
- 读写互斥锁:
- 如果场景中读操作远多于写操作,可以使用
sync.RWMutex
(读写锁) - 多个读操作可以同时获取读锁(
RLock()
/RUnlock()
) - 写操作需要获取写锁(
Lock()
/Unlock()
),此时会阻塞所有读和写
- 如果场景中读操作远多于写操作,可以使用
6.读写互斥锁
如果写文件的时候只允许一个执行,读可以多个协程操作。
var rwmu sync.RWMutex
var data map[string]int// 读操作:可以并发执行
func readKey(key string) int {rwmu.RLock()defer rwmu.RUnlock() //延时执行return data[key]
}// 写操作:独占访问
func writeKey(key string, value int) {rwmu.Lock()defer rwmu.Unlock() //延时执行data[key] = value
}
十五、反射
1.反射基本使用
Go 语言的反射(reflection)是一种能够在程序运行时检查、访问和修改变量类型与值的机制,主要通过
reflect
包实现。反射常用于处理未知类型的变量(如 JSON 序列化 / 反序列化、ORM 框架等场景)。通过reflect.TypeOf()
和reflect.ValueOf()
函数可以分别获取这两个类型的实例。
reflect.Type
:表示变量的静态类型信息reflect.Value
:表示变量的动态值信息
反射可以做的事:
- 反射可以在程序运行期间动态获取变量的各种信息。例如变量的类型
- 结构体通过反射可以获取结构体本身的信息,比如字段和方法
- 反射可以修改变量的值,调用关联的方法
1.获取类型信息(reflect.Type
)
package mainimport ("fmt""reflect"
)func main() {var num int = 42var str string = "hello"// 获取类型信息t1 := reflect.TypeOf(num)t2 := reflect.TypeOf(str)fmt.Println("num的类型:", t1.Name()) // intfmt.Println("str的类型:", t2.Name()) // stringfmt.Println("num是否为int:", t1.Kind() == reflect.Int) // true
}
Kind()
方法返回变量的基础类型(如 int
、string
、slice
等),而 Name()
返回类型名称(自定义类型会返回其定义名)。
2.获取值信息(reflect.Value
)
var num int = 42
v := reflect.ValueOf(num)fmt.Println("值:", v.Int()) // 42(获取int值)
fmt.Println("是否可修改:", v.CanSet()) // false(因为传递的是副本)
注意:reflect.ValueOf()
接收的是变量的副本,默认情况下无法修改原变量的值。
3. 修改变量的值(需要指针)
要修改原变量的值,必须传递指针并通过 Elem()
方法获取指针指向的元素:
var num int = 42
v := reflect.ValueOf(&num) // 传递指针// 判断是否为指针
if v.Kind() == reflect.Ptr {// 获取指针指向的元素(解引用)elem := v.Elem()if elem.CanSet() { // 现在可以修改了elem.SetInt(100) // 设置新值}
}fmt.Println(num) // 100(原变量已被修改)
2.反射的常见方法
类别 | 方法 | 所属类型 | 说明 |
---|---|---|---|
获取反射对象 | reflect.TypeOf(v) | 函数 | 获取变量 v 的类型信息(返回 reflect.Type ) |
获取反射对象 | reflect.ValueOf(v) | 函数 | 获取变量 v 的值信息(返回 reflect.Value ) |
基础类型信息 | Kind() | Type/Value | 返回基础类型(如 reflect.Int 、reflect.Struct ) |
基础类型信息 | Name() | Type | 返回类型名称(自定义类型有效,基础类型可能为空) |
基础类型信息 | String() | Type/Value | 返回类型 / 值的字符串表示 |
基础值操作 | Interface() | Value | 将反射值转换为 interface{} 类型 |
基础值操作 | IsValid() | Value | 判断反射值是否有效(无效如:未初始化的 Value ) |
基础值操作 | IsNil() | Value | 判断值是否为 nil (仅对指针、切片等引用类型有效) |
结构体操作 | NumField() | Type | 返回结构体字段数量 |
结构体操作 | Field(i int) | Type | 返回结构体第 i 个字段的 StructField 信息 |
结构体操作 | FieldByName(name string) | Type | 通过名称查找结构体字段(返回字段信息和是否找到) |
结构体操作 | FieldByIndex(index []int) | Type | 通过索引链(如嵌套结构体 [0][1] )查找字段 |
方法操作 | NumMethod() | Type/Value | 返回类型 / 值的方法数量 |
方法操作 | Method(i int) | Type/Value | 返回第 i 个方法的信息(Type 返回 Method ,Value 返回可调用 Value ) |
方法操作 | MethodByName(name string) | Type/Value | 通过名称查找方法 |
方法操作 | Call(args []Value) | Value | 调用方法,参数为 []Value ,返回结果 []Value |
指针 / 元素操作 | Elem() | Type/Value | 对指针、切片等,返回其指向的元素类型 / 值(解引用) |
修改值操作 | CanSet() | Value | 判断是否可修改值(通常需指针解引用后为 true ) |
修改值操作 | Set(v Value) | Value | 将值设置为另一个 reflect.Value |
修改值操作 | SetInt(x int64) | Value | 设置为 int 类型值(类似:SetUint /SetFloat /SetBool /SetString ) |
容器类型操作 | Len() | Type/Value | 返回数组长度(Type)或切片 / 字符串长度(Value) |
容器类型操作 | Index(i int) | Value | 返回切片 / 数组中第 i 个元素的值 |
Map 操作 | MapKeys() | Value | 返回 map 中所有键的值([]Value ) |
Map 操作 | MapIndex(key Value) | Value | 返回 map 中指定键对应的值 |
Map 操作 | SetMapIndex(key, val Value) | Value | 向 map 中设置键值对(val 为零值时删除键) |