16 KiB
16 KiB
1-go基础
- https://go.dev/tour/ (必读) , 中文版 https://tour.go-zh.org/
- https://go.dev/doc/effective_go (必读)
- https://github.com/Unknwon/the-way-to-go_ZH_CN
初始化
go mod init github.com/用户名/模块名
# 会生成go.mod, 相当于 npm 的 package.json
# 删除不再依赖的模块
go mod tidy
// hello-word.go
package main
import "fmt" // 标准库
func main() {
fmt.Println("Hello World")
}
运行 go run ./hello-word.go
import (
"fmt"
"math"
) // 分组导入
在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。例如,Pizza
就是个已导出名,Pi
也同样,它导出自 math
包。
打印信息
Print: 轻量, 没有时间戳
开发阶段调试或者简单输出文本
fmt.Println("Hello, World!") // 在末尾添加换行符
fmt.Print("Hello, ") // 不添加换行符号
fmt.Print("World!\n")
fmt.Printf("Name: %s, Age: %d\n", "Alice", 30) // 格式化字符串
Log:
默认带有时间戳, 可以输出日志所在的文件和行号, 可以触发程序的终止或异常
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`,程序会抛出异常。适用于需要立即中断的错误
变量
var a = 1 // var声明变量,并赋值; 全局变量
func main() {
var b int // 声明变量后面再赋值,就得带上类型
b = 2
fmt.Println(b)
str := "Hello World" // 函数里的变量 := 直接赋值,自动推断类型
fmt.Println(str)
}
:=
结构不能在函数外使用- 没有明确初始化的变量声明会被赋予对应类型的 零值。
- 数值类型零值为
0
, - 布尔类型零值为
false
, - 字符串零值为
""
(空字符串)。
- 数值类型零值为
- 常量
//常量的声明与变量类似,只不过使用 const 关键字。
//常量可以是字符、字符串、布尔值或数值。
//常量不能用 := 语法声明。
const Pi = 3.14
基本类型
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 在不同类型的项之间赋值时需要显式转换
i := 42
f := float64(i)
u := uint(f)
运算符
// 加减乘除 + - * /
// 取余 %
// 比较 == != < <= > >=
// 按位与 &
// 逻辑运算符 && ||
// 逻辑非 !
分支
if else
- 表达式外无需小括号
( )
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
// 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
语句后面的三个构成部分外没有小括号
- 和 C、Java、JavaScript 之类的语言不同,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 等):
// 同时需要索引和值
for index, value := range array {
// 使用 index 和 value
}
// 只需要值
for _, value := range array {
// 只使用 value
}
// 只需要索引
for index := range array {
// 只使用 index
}
模拟while循环
// 模拟while循环
m := 1
for m < 5 {
fmt.Println(m)
m++
}
函数
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 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
数组 array
同一类型的元素的固定长度序列
- 在实践中,切片比数组更常用。
// 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
切片是对数组的抽象,它提供了更强大的功能和灵活性 切片的长度可以动态变化,这是它与数组的主要区别
- 切片就像数组的引用, 切片并不存储任何数据, 更改切片的元素会修改其底层数组中对应的元素, 和它共享底层数组的切片都会观测到这些修改。
// 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)
切片的默认行为
var a [10]int
// 以下切片表达式和它是等价的
a[0:10]
a[:10]
a[0:]
a[:]
切片的长度 len 和容量 cap
- 切片的长度就是它所包含的元素个数。
- 切片的容量是从==切片的起始==位置到==底层数组==的末尾
- 切片
s
的长度和容量可通过表达式len(s)
和cap(s)
来获取。
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)
}
输出
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 的元素。- 切片操作不会改变底层数组的容量,只是改变了切片的长度和起始位置。
使用场景:
- 预分配切片:在一些场景中,你可能知道要存储的元素个数并且希望避免动态扩展切片,可以通过预分配足够的容量来避免性能损失。
data := make([]int, 50) // 长度和容量都为50
- 优化性能:当你需要向切片中添加元素时,如果切片的容量已经满了,Go会创建一个新的底层数组并将现有元素复制过去。这是一个相对耗时的操作。如果你提前知道大致需要的容量,可以预先分配好,避免多次内存分配和数据复制操作。
// 预先分配容量,避免多次扩容
s := make([]int, 0, 100) // 长度为0,容量为100
for i := 0; i < 100; i++ {
s = append(s, i)
}
- 切片的高效操作:利用切片的容量可以进行更高效的切片操作,而不必担心底层数组的频繁重新分配
// 假设有一个初始切片
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 且没有底层数组
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
二维切片
切片可以包含任何类型,当然也包括其他切片。
// 创建一个井字棋(经典游戏)
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倍
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)
}
// 输出
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
循环遍历切片时,每次迭代都会返回两个值。 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
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)
}
}
可以将下标或值赋予 _
来忽略它。
for i, _ := range pow
for _, value := range pow
若你只需要索引,忽略第二个变量即可。
for i := range pow
映射 map
一种无序的键值对集合。映射的键必须是唯一的,且通常是可比较的类型(如字符串、数字等) 相当于 js 里的对象
// 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)
删除元素
delete(m, key)
检测某个键是否存在
elem, ok = m[key]
// 若 `key` 在 `m` 中,`ok` 为 `true` ;否则,`ok` 为 `false`。
// 若 `key` 不在映射中,则 `elem` 是该映射元素类型的零值。
结构体 struct
// 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)
结构体添加方法
// 结构体也能添加方法
type Point struct {
X int
Y int
}
func (p *Point) SetPoint(x, y int) { // 大部分情况需要*,否则只是修改副本;方法名首字母大写; 方法是定义在结构体的外部
p.X = x
p.Y = y
}
接口
// 接口
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()
}
错误处理
func main() {
// 错误处理
n, err := fmt.Println("Hello") // 错误是通过函数的多个返回值实现的
// 需要判断err是否为nil或者有没有值
if err != nil {
// 执行正常代码
} else {
// 执行异常代码
}
}
多线程
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)
}
}
}
模块管理
go get github.com/hekmon/transmissionrpc/v3
# 如何卸载
# 删除 go.mod 中的依赖行
# 清理未使用的依赖
go mod tidy
Vscode 断点调试
https://pengtech.net/golang/vscode_debug_golang.html
- 安装
delve
https://github.com/go-delve/delve/tree/master/Documentation/installation
which dlv
- 点击调试图标, 创建
.vscode/launch.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"
}
]
}