devStandard/docs/learning/a-go/1-go基础.md
2025-03-29 14:35:49 +08:00

775 lines
16 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 1-go基础
1. [https://go.dev/tour/](https://go.dev/tour/) (必读) , 中文版 https://tour.go-zh.org/
2. [https://go.dev/doc/effective_go](https://go.dev/doc/effective_go) (必读)
3. https://github.com/Unknwon/the-way-to-go_ZH_CN
## 初始化
```sh
go mod init github.com/用户名/模块名
# 会生成go.mod, 相当于 npm 的 package.json
# 删除不再依赖的模块
go mod tidy
```
```go
// hello-word.go
package main
import "fmt" // 标准库
func main() {
fmt.Println("Hello World")
}
```
运行 `go run ./hello-word.go`
```go
import (
"fmt"
"math"
) // 分组导入
```
在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。例如,`Pizza` 就是个已导出名,`Pi` 也同样,它导出自 `math` 包。
## 打印信息
Print: 轻量, 没有时间戳
> 开发阶段调试或者简单输出文本
```go
fmt.Println("Hello, World!") // 在末尾添加换行符
fmt.Print("Hello, ") // 不添加换行符号
fmt.Print("World!\n")
fmt.Printf("Name: %s, Age: %d\n", "Alice", 30) // 格式化字符串
```
Log:
默认带有时间戳, 可以输出日志所在的文件和行号, 可以触发程序的终止或异常
```go
log.Println("This is a log message.")
log.Print("Logging without newline.")
log.Printf("Name: %s, Age: %d", "Alice", 30)
log.Fatal("Fatal error occurred.") // 打印信息后调用 os.Exit(1),程序会立即终止。适用于严重错误
log.Panic("Panic occurred.") // 打印信息后调用 `panic`,程序会抛出异常。适用于需要立即中断的错误
```
## 变量
```go
var a = 1 // var声明变量,并赋值; 全局变量
func main() {
var b int // 声明变量后面再赋值,就得带上类型
b = 2
fmt.Println(b)
str := "Hello World" // 函数里的变量 := 直接赋值,自动推断类型
fmt.Println(str)
}
```
* `:=` 结构不能在函数外使用
* 没有明确初始化的变量声明会被赋予对应类型的 **零值**
* 数值类型零值为 `0`
* 布尔类型零值为 `false`
* 字符串零值为 `""`(空字符串)。
* 常量
```go
//常量的声明与变量类似,只不过使用 const 关键字。
//常量可以是字符、字符串、布尔值或数值。
//常量不能用 := 语法声明。
const Pi = 3.14
```
## 基本类型
```go
func main() {
a := 10
fmt.Printf("a 类型为: %T\n", a) // Printf格式化输出, %T占位
b := float32(10)
fmt.Printf("b 类型为: %T\n", b)
// 基本类型
// 整数
// int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex64
// byte // uint8 别名
// rune // int32 别名
// 浮点数
// float32 float64
// 复数
// complex64 complex128
// 布尔类型
// bool
// 字符串
// string
}
```
* 类型转换: Go 在不同类型的项之间赋值时需要显式转换
```go
i := 42
f := float64(i)
u := uint(f)
```
## 运算符
```go
// 加减乘除 + - * /
// 取余 %
// 比较 == != < <= > >=
// 按位与 &
// 逻辑运算符 && ||
// 逻辑非 !
```
## 分支
### if else
* 表达式外无需小括号 `( )`
```go
func main() {
a := 10
if a > 10 {
fmt.Println("a > 10")
} else if a > 5 {
fmt.Println("5 < a <= 10")
} else {
fmt.Println("a <= 5")
}
// 局部变量声明
// 还能在if中直接用 := 赋值 ; 该语句声明的变量作用域仅在 `if` 和 `else` 之内。
if b := 10; b > 10 {
fmt.Println("b > 10")
}
}
```
### switch
```go
// switch语句, case里不需要写break
c := 10
switch c {
case 10:
fmt.Println("c == 10")
case 5:
fmt.Println("c == 5")
default:
fmt.Println("c != 10 && c != 5")
}
```
## 循环
### for循环
* 三参数形式
* 和 C、Java、JavaScript 之类的语言不同Go 的 `for` 语句后面的三个构成部分外没有小括号
```go
// 只有一种循环形式
// for循环
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// 可以用break来退出循环
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Println(i)
}
// 可以用continue来跳过当前循环
for i := 0; i < 10; i++ {
if i == 5 {
continue
}
fmt.Println(i)
}
}
```
* range 形式用于遍历数组、切片、map 等):
```go
// 同时需要索引和值
for index, value := range array {
// 使用 index 和 value
}
// 只需要值
for _, value := range array {
// 只使用 value
}
// 只需要索引
for index := range array {
// 只使用 index
}
```
### 模拟while循环
```go
// 模拟while循环
m := 1
for m < 5 {
fmt.Println(m)
m++
}
```
## 函数
```go
func sum(a int, b int) int {
return a + b
}
func swap(a, b int) (int, int) { // a b的类型是同一种函数可以返回多个值
return b, a
}
func main() {
fmt.Println(sum(1, 2))
a, b := swap(1, 2)
fmt.Println(a, b)
}
```
没有参数的 `return` 语句会直接返回已命名的返回值,也就是「裸」返回值。
### defer 推迟
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
```go
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
```
## 数组 array
> **同一类型**的元素的**固定长度**序列
* 在实践中,切片比数组更常用。
```go
// Array
var arr [5]int // 声明一个包含5个整数的数组
arr[0] = 10 // 将数组的第一个元素设置为10
fmt.Println(arr[0]) // 输出: 10
a := [3]int{1, 2, 3} // 数据的长度是固定且不可变的
fmt.Println(a)
```
## 切片 slice
> 切片是对数组的抽象,它提供了更强大的功能和灵活性
> **切片的长度可以动态变化**,这是它与数组的主要区别
* 切片就像数组的引用, 切片并不存储任何数据, 更改切片的元素会修改其底层数组中对应的元素, 和它共享底层数组的切片都会观测到这些修改。
```go
// Slice
// 如果要定义可变长度的数组可以使用slice
var slice []int // 声明一个整数切片, 这里的方括号里没有长度, 数组则是要求固定长度
slice = make([]int, 5) // 创建一个长度为5的切片
// 切片也可以通过数组的一部分创建
arr := [5]int{1, 2, 3, 4, 5}
slice2 := arr[1:4] // 创建一个包含arr[1]到arr[3]的切片
// 用make函数创建, 初始长度为0
b := make([]int, 0)
//`make` 函数会分配一个元素为零值的数组并返回一个引用了它的切片
e := make([]int, 5) // len(e)=5, cap(e)=5, [0,0,0,0,0]
// 要指定它的容量,需向 make 传入第三个参数
a := make([]int, 0, 5) // len(a)=0, cap(a)=5, []
f := e[:2] // len(f)=2, cap(f)=5, [0, 0]
g := f[2:5]
// 追加元素
b = append(b, 1, 2, 3)
fmt.Println(b)
// 或者字面值
c := []int{1, 2, 3} // []里不指定长度
c[0] = 10 // 使用下标的方式修改
fmt.Println(c)
// 选出一个半闭半开区间,包括第一个元素,但排除最后一个元素。
d := a[1:3]
fmt.Println("d的值为:", d)
```
### 切片的默认行为
```go
var a [10]int
// 以下切片表达式和它是等价的
a[0:10]
a[:10]
a[0:]
a[:]
```
### 切片的长度 len 和容量 cap
* 切片的长度就是它所包含的元素个数。
* 切片的容量是从==切片的起始==位置到==底层数组==的末尾
* 切片 `s` 的长度和容量可通过表达式 `len(s)` 和 `cap(s)` 来获取。
```go
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// 截取切片使其长度为 0
s = s[:0]
printSlice(s)
// 扩展其长度
s = s[:4]
printSlice(s)
// 舍弃前两个值
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
```
输出
```go
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]
```
* `s[:i]` 创建一个从切片 s 的开始到索引 i 的新切片,不包括索引 i 的元素。
* `s[i:]` 创建一个从索引 i 到切片 s 的末尾的新切片,包括索引 i 的元素。
* 切片操作不会改变底层数组的容量,只是改变了切片的长度和起始位置。
使用场景:
* **预分配切片**:在一些场景中,你可能知道要存储的元素个数并且希望避免动态扩展切片,可以通过预分配足够的容量来避免性能损失。
```go
data := make([]int, 50) // 长度和容量都为50
```
* 优化性能当你需要向切片中添加元素时如果切片的容量已经满了Go会创建一个新的底层数组并将现有元素复制过去。这是一个相对耗时的操作。如果你提前知道大致需要的容量可以预先分配好避免多次内存分配和数据复制操作。
```go
// 预先分配容量,避免多次扩容
s := make([]int, 0, 100) // 长度为0容量为100
for i := 0; i < 100; i++ {
s = append(s, i)
}
```
* 切片的高效操作:利用切片的容量可以进行更高效的切片操作,而不必担心底层数组的频繁重新分配
```go
// 假设有一个初始切片
s := make([]int, 0, 10) // 长度为0容量为10
// 可以安全地追加元素,直到达到容量限制
for i := 0; i < 10; i++ {
s = append(s, i)
}
// 如果继续追加元素,则会触发容量的扩展
s = append(s, 10) // 触发扩展,容量将翻倍
```
### nil 的切片
切片的零值是 `nil`。 (零值: 没有赋值时的默认值)
nil 切片的长度和容量为 0 且没有底层数组
```go
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
```
### 二维切片
切片可以包含任何类型,当然也包括其他切片。
```go
// 创建一个井字棋(经典游戏)
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
// 两个玩家轮流打上 X 和 O
board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"
for i := 0; i < len(board); i++ {
fmt.Printf("%s\n", strings.Join(board[i], " "))
}
```
### 切片追加元素
切片slice的容量是动态增长的。当你向切片追加元素时如果当前容量不足以容纳新元素Go会自动扩容切片。切片的容量增长策略通常是当前容量的1.25倍
```go
package main
import "fmt"
func main() {
var s []int
printSlice(s)
// 可在空切片上追加
s = append(s, 0)
printSlice(s)
// 这个切片会按需增长
s = append(s, 1)
printSlice(s)
// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
```
```go
// 输出
len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=5 cap=6 [0 1 2 3 4]
```
分析添加 2,3,4的过程
```
初始状态长度2容量2。
追加第一个元素2长度变为3容量变为4。
追加第二个元素3长度变为4容量仍为4。
追加第三个元素4长度变为5容量变为6。
```
### range遍历
`for` 循环的 `range` 形式可遍历切片或映射。
当使用 `for` 循环遍历切片时,每次迭代都会返回两个值。 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
```go
package main
import "fmt"
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
```
可以将下标或值赋予 `_` 来忽略它。
```go
for i, _ := range pow
for _, value := range pow
```
若你只需要索引,忽略第二个变量即可。
```go
for i := range pow
```
## 映射 map
> 一种无序的键值对集合。映射的键必须是唯一的,且通常是可比较的类型(如字符串、数字等)
> 相当于 js 里的对象
```go
// Map
var m1 map[string]int // 声明一个键为字符串、值为整数的映射
m1 = make(map[string]int) // 初始化映射
m := map[string]int{"a": 1, "b": 2, "c": 3} // []里是key的类型,后面是value的类型
fmt.Println(m)
fmt.Println(m["a"])
// 如果不需要初始值, 也可以用make函数创建
// 映射的零值是nil
n := make(map[string]int)
n["a"] = 1
n["b"] = 2
n["c"] = 3
fmt.Println(n)
```
### 删除元素
```go
delete(m, key)
```
### 检测某个键是否存在
```go
elem, ok = m[key]
// 若 `key` 在 `m` 中,`ok` 为 `true` ;否则,`ok` 为 `false`。
// 若 `key` 不在映射中,则 `elem` 是该映射元素类型的零值。
```
## 结构体 struct
```go
// Struct 结构体, 类似于js里的class类
type Student struct {
Name string
Age int
Sex string
}
s := Student{"jqtm", 18, "male"}
t := s
s.Age = 19 // 使用.修改值
fmt.Println(s)
fmt.Println(t) // s的修改不会影响t,因为t是s的副本
// Go 语言中结构体是值类型,赋值操作会创建一个新的副本,而不是引用
// 如果想要引用传递,可以使用指针
u := &s // go中通过&来获取变量指针地址
s.Age = 20
fmt.Println(s)
fmt.Println(*u) // 使用*来解引用
u.Age = 21 // 修改值时可以省略*, go会自动进行解引用
fmt.Println(s)
fmt.Println(*u) // 使用*来声明这是个指针,进行解引用
p := Point{1, 2}
p.SetPoint(3, 4)
fmt.Println(p)
```
### 结构体添加方法
```go
// 结构体也能添加方法
type Point struct {
X int
Y int
}
func (p *Point) SetPoint(x, y int) { // 大部分情况需要*,否则只是修改副本;方法名首字母大写; 方法是定义在结构体的外部
p.X = x
p.Y = y
}
```
## 接口
```go
// 接口
type Shape interface { // type 接口名 interface
Print() // 在里面定义方法,不需要func
}
// 结构体实现接口
type Rectangle struct{}
type Circle struct{}
// 定义同名的方法
func (r Rectangle) Print() {
fmt.Println("矩形")
}
func (c Circle) Print() {
fmt.Println("圆形")
}
func main() {
//接口的实现
var i Shape // 类型是接口
i = Rectangle{} // 赋值不同的结构,实现多态
i.Print()
i = Circle{}
i.Print()
}
```
## 错误处理
```go
func main() {
// 错误处理
n, err := fmt.Println("Hello") // 错误是通过函数的多个返回值实现的
// 需要判断err是否为nil或者有没有值
if err != nil {
// 执行正常代码
} else {
// 执行异常代码
}
}
```
## 多线程
```go
func func1() {
time.Sleep(500 * time.Millisecond) // Millisecond 毫秒
fmt.Println("func1")
}
func func2() {
fmt.Println("func2")
}
func func3(ch chan string) {
time.Sleep(2 * time.Second)
ch <- "func3" // 发送数据到管道
}
func func4(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "func4"
}
func main() {
// 多线程 Goroutines
go func2() // 开启一个新的线程
func1() // 在主线程中执行,延迟了500毫秒,func2能够在主线程退出前打印
// 管道 Channels, 多个线程之间进行通信和同步
ch := make(chan string) // 使用make函数创建一个管道
go func3(ch) // 需要开启一个新的线程来接收数据
res1 := <-ch // 从管道中获取数据,接收操作是阻塞的,直到有数据发送过来
fmt.Println(res1) // 所以是先打印func3再打印func4
go func4(ch)
res2 := <-ch
fmt.Println(res2)
go func3(ch)
go func4(ch)
for i := 0; i < 2; i++ {
select { // select 可以同时接收多个管道数据,类似于switch
case res := <-ch:
fmt.Println(res)
}
}
}
```
## 模块管理
```sh
go get github.com/hekmon/transmissionrpc/v3
# 如何卸载
# 删除 go.mod 中的依赖行
# 清理未使用的依赖
go mod tidy
```
## Vscode 断点调试
https://pengtech.net/golang/vscode_debug_golang.html
1. 安装 `delve`
```sh
https://github.com/go-delve/delve/tree/master/Documentation/installation
which dlv
```
1. 点击调试图标, 创建 `.vscode/launch.json`
```json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch golang program",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "main.go"
}
]
}
```