GoLang/Wails学习笔记

GoLang/Wails学习笔记

小叶子

封面作者:NOEYEBROW

请先阅读JavaScript学习笔记TyepScript学习笔记

⭐Go

Go 是一种并发支持、垃圾回收的编译型系统编程语言,旨在创造一种具有静态编译语言的高性能和动态语言的高效开发之间的理想平衡

官网 下载安装包,安装完成后,可以在终端输入 go version 查看版本号并确认安装成功

  • Go 的注释与 JavaScript 相同; 变量只能由字母、数字、下划线组成,且不能以数字开头; 变量和函数采用驼峰命名法
  • Go 的运算符和 JavaScript 也类似, 但是 ++-- 必须后置; 数字也和 JavaScript 一样,可以有 _ 分隔符和不同进制
  • Go 的字符字面量使用 '', 支持 Unicode; 字符串字面量使用 ""``
  • 你说得对, 但是 if err != nil
1
2
3
4
# 运行
go run main.go
# 编译
go build main.go

go mod

Go 1.11 版本之后引入了 go mod 包管理工具, 可以在项目中使用, 用于管理项目的依赖; 可以在https://pkg.go.dev/ 上查找依赖

go.mod 相当于 package.json; go.sum 相当于 package-lock.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 在 GitHub 上创建一个新的仓库
git clone xxx
# 初始化模块
go mod init github.com/xxx/xxx

# 安装依赖
go mod download
# 添加依赖
go get github.com/xxx/xxx
# 移除依赖
go get github.com/xxx/xxx@none

# 安装需要的依赖, 移除不需要的依赖
go mod tidy

# 安装命令行程序, 相当于 npm install -g
go install github.com/xxx/xxx
# 运行命令行程序, 相当于 npx xxx
go run github.com/xxx/xxx

导入导出

Go 中的包可能包含多个文件,但是只能有一个 main 包,main 包是程序的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main // 包声明

import "fmt" // 引入包
import o "os" // 引入包并重命名
import (
"fmt" // 批量引入包
_ "os" // 匿名引入, 通常是用于调用包中的 init 函数
)

func main() { // 入口函数
fmt.Println("Hello, World!")
}

// Go 禁止循环导入包
1
2
3
4
5
6
7
8
9
10
11
12
13
package demo

import "fmt"

func privateFunc() {
fmt.Println("privateFunc")
} // 小写字母开头的函数为私有函数

func PublicFunc() {
fmt.Println("PublicFunc")
} // 大写字母开头的函数为公有函数

// 该规则适用于变量、常量、结构体、接口等

数据类型

类型描述示例
bool布尔型true, false
uint8/16/32/64无符号整型0, 255
int8/16/32/64有符号整型-128, 127
uint/int相当于 Rust 中的 usize0, 255
uintptr无符号整型,用于存放一个指针0x123456
float32/64浮点型3.14, 0.618
complex64/128复数3.14+0i, 0.618+0i
byteuint8 的别名, 表示 ASCIIbyte('A')
runeint32 的别名, 表示 Unicoderune('中')
string字符串, 可以转为 []byte"Hello, World!"
[]T切片, 动态数组[]int{1, 2, 3}
[n]T数组, 固定长度数组[3]int{1, 2, 3}, n 必须是常量
map[K]V映射, 键值对map[string]int{"a": 1, "b": 2}
struct结构体, 自定义类型type Person struct { Name string; Age int }
interface接口, 抽象类型type Animal interface { Eat() }
func函数, 函数类型func Add(a, b int) int { return a + b }
chan通道, 用于协程间通信 (后面细讲)ch := make(chan int)
*T指针, 指向 T 类型的指针 (后面细讲)var p *int = &a
  • 类型转换: int(3.14)(func() int)(xxx) 等, 不存在隐式类型转换
  • 类型断言: value.(int), 该语句返回 转换后的值, 转换是否成功 两个返回值; 常用于判断接口变量的实际类型
  • 类型判断: value.(type), 该语句只能用于 switch 语句中, 用于判断接口变量的实际类型
1
2
3
4
5
6
7
8
var a float64 = 3.14
var b int = int(a)
switch a.(type) {
case int:
fmt.Println("int")
case float64:
fmt.Println("float64")
} // float64

默认值

不同于 JavaScript, Go 变量即使没有赋值也会有默认值

类型默认值 (零值)
boolfalse
数字0
string""
数组对应类型的零值, 如 [3]int[0, 0, 0]
struct对应类型的零值, 如 PersonPerson{}
其他nil

nilnull 不同, 其本身不属于任何类型

自定义类型

1
2
type MyInt int
var num MyInt = 123

通常用于类型别名 (有的库的类型名很长)、附加方法、声明结构体字段等

常量

常量的值不能在运行时修改 (即被写死在二进制文件中), 其值只能说基本数据类型, 可能来源于字面量、其他常量标识符、常量表达式等; 常量的类型可以省略

1
2
3
4
5
6
7
8
9
10
const constNum = 123
const constStr = "Hello, World!"
const constExp = 1 + 2 + constNum

// 批量声明
const (
constA = 1
constB = 2
constC = 3
)

iota

iotaGo 语言的常量计数器, 只能在常量的表达式中使用, 且每次使用 iota 时都会自增 1

1
2
3
4
5
6
7
8
9
10
11
12
13
const (
Num = iota // 0
Num1 // 1
Num2 // 2
)

const (
Num = iota*2 // 0
Num1 // 2
Num2 // 4
Num3 = iota // 3
Num4 // 4
)

变量

变量的值可以在运行时修改, 其值可以是任意类型, 但是类型一旦确定就不能修改; Go 的类型推断必须通过 := 手动进行

1
2
3
4
5
6
7
8
9
10
11
12
13
var varNum int = 123
var varStr string = "Hello, World!"
var (
varA int = 1
varB string = "Hello"
)
var numA, numB, numC int = 1, 2, 3

// 类型推断, 此时应省略 var
varNum := 123
varStr := "Hello, World!"

// 由于 nil 不属于任何类型, 所以不能使用类型推断

Go 中的变量声明必须使用, 否则会报错; 如果确实不需要使用, 可以使用 _ 占位符

解构赋值

1
2
3
4
var a, b := 1, 2
fmt.Println(a, b) // 1, 2
a, b = b, a
fmt.Println(a, b) // 2, 1

作用域

Go 中可以手动用 {} 创建作用域, 作用域内的变量只能在作用域内使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

var a = 1

func main() {
fmt.Println(a) // 1
{
var a = 2
fmt.Println(a) // 2
}
fmt.Println(a) // 1
}

输入输出

函数描述
fmt.Print(xxx)打印, 不换行
fmt.Println(xxx)打印, 换行
fmt.Printf(xxx, var)打印, 格式化输出
fmt.Scan(&var)输入, 根据空格或换行符分割
fmt.Scanln(&var)输入, 根据换行符分割
fmt.Scanf(xxx, &var)输入, 根据格式化输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var (
num int
str string
)
fmt.Print("请输入一个整数: ")
fmt.Scan(&num)
fmt.Print("请输入一个字符串: ")
fmt.Scanln(&str)
fmt.Printf("num: %d, str: %s\n", num, str)
}

格式化

格式化描述接受类型
%%百分号
%s字符串string / []byte
%d十进制整数各种整数类型
%f浮点数float32/64
%t布尔值bool
%v值原本的形式,多用于数据结构的输出任意类型
%#v值的 Go 语法表示任意类型
%+v类似 %v, 但输出结构体时会添加字段名任意类型
%T值的类型任意类型
%p指针指向的地址*T

条件语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// if else
if num > 0 {
fmt.Println("num > 0")
} else if num < 0 {
fmt.Println("num < 0")
} else {
fmt.Println("num = 0")
}

// if 初始化语句
if msg := "Hello"; num > 0 {
fmt.Println(msg)
} // msg 只在 if 作用域内有效

// switch
switch num {
case 1:
fmt.Println("num = 1")
case 2:
fmt.Println("num = 2")
default:
fmt.Println("num = 0")
}
str := "Hello"
switch { // 相当于 switch true
case str == "Hello":
fmt.Println("str = Hello")
case str == "World":
fmt.Println("str = World")
default:
fmt.Println("str = None")
}

// goto, 别用, 可读性差
for i := 0; i < 10; i++ {
if i == 5 {
goto end
}
fmt.Println(i)
}
end: // 标签语句
fmt.Println("End")

循环语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// for
for i := 0; i < 10; i++ {
fmt.Println(i)
}
for i < 10 { // 相当于 while
fmt.Println(i)
i++
}
for { // 相当于 loop
fmt.Println(i)
i++
if i == 10 {
break
}
}

// range
arr := []int{1, 2, 3}
for index, value := range arr {
fmt.Printf("index: %d, value: %d\n", index, value)
}

// break, continue
for i := 1; i != 0; i++ {
if i <= 5 {
continue
}
if i >= 10 {
break
}
fmt.Println(i)
} // 6, 7, 8, 9

// 通过标签语句跳出多层循环
end:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i == 5 && j == 5 {
break end
}
fmt.Println(i, j)
}
}

数组和切片

Go 中的数组是值类型, 作为参数传递时会拷贝一份, 但是切片是引用类型, 传递时只会传递指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 数组
var arr [3]int
arr[0] = 1
arr[1] = 2
fmt.Println(arr) // [1 2 0]
fmt.Println(len(arr)) // 3
fmt.Println(cap(arr)) // 3, 对于数组来说, 容量和长度相同

// 切片
slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice) // [1 2 3 4 5]
fmt.Println(slice[1:3]) // [2 3], 左闭右开
fmt.Println(slice[:3]) // [1 2 3], 从头开始
fmt.Println(slice[3:]) // [4 5], 到尾结束
fmt.Println(len(slice)) // 5

slice := make([]int, 3, 5) // 创建一个长度为 3, 容量为 5 的切片
fmt.Println(slice) // [0 0 0]
fmt.Println(len(slice)) // 3
fmt.Println(cap(slice)) // 5

// 添加元素
slice = append(slice, 1, 2, 3)
fmt.Println(slice) // [0 0 0 1 2 3]
// 从头添加
slice = append([]int{1, 2, 3}, slice...) // 展开运算符 ...
fmt.Println(slice) // [1 2 3 0 0 0 1 2 3]
// 从 i 位置添加
slice = append(slice[:i+1], append([]int{1, 2, 3}, slice[i+1:]...)...)
fmt.Println(slice) // [1 2 3 0 1 2 3 0 0 0 1 2 3]

// 删除元素
slice = slice[:5] // 删除尾部元素
fmt.Println(slice) // [1 2 3 0 1]
slice = slice[1:] // 删除头部元素
fmt.Println(slice) // [2 3 0 1]
slice = append(slice[:i], slice[i+1:]...) // 删除中间元素
fmt.Println(slice) // [2 3 1]
slice = slice[:0] // 清空切片
// 或 clear(slice)
fmt.Println(slice) // []

// 复制切片
oldSlice := []int{1, 2, 3}
newSlice := make([]int, len(oldSlice))
copy(newSlice, oldSlice)

// 多维切片
slice := [][]int{{1, 2}, {3, 4}}
fmt.Println(slice) // [[1 2] [3 4]]

拓展表达式

1
2
3
4
5
6
7
8
func main() {
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4:4] // cap = 4 - 3 = 1
// 容量不足,分配新的底层数组
s2 = append(s2, 1)
fmt.Println(s2) // [4 1]
fmt.Println(s1) // [1 2 3 4 5 6 7 8 9]
}

字符串

Go 中的字符串的本质是 [n]byte, 可以使用数组和切片的方式进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
str := "Hello, World!"
fmt.Println(str[0]) // 72, ASCII 码
fmt.Println(string(str[0])) // H
fmt.Println(string(str[0:5])) // Hello
// 不能直接修改字符串中的字符
str[0] = 'h' // 报错
str = "hello" // 正确

// 转为 []byte
byteStr := []byte(str)
fmt.Println(byteStr) // [104 101 108 108 111]
fmt.Println(string(byteStr)) // hello
// 可以修改 []byte 中的字符
byteStr[0] = 'H'
byteStr = append(byteStr, []byte(" World!")...)
fmt.Println(string(byteStr)) // Hello World!
fmt.Println(len(byteStr)) // 12
// 注意: 一个中文字符占 3 个字节

// 拷贝
oldStr := "Hello"
newStr := make([]byte, len(oldStr))
copy(newStr, oldStr)
// 或者
newStr = strings.Clone(oldStr)

// 字符串拼接
str1 := "Hello"
str2 := "World"
str3 := str1 + " " + str2
// 高性能拼接
builder := strings.Builder{}
builder.WriteString(str1)
builder.WriteString(" ")
builder.WriteString(str2)
str3 = builder.String()

要遍历打印字符串中的 Unicode 字符, 需要格式化为 rune 类型 (%c)

映射

Go 中的映射是无序的键值对集合, 键值对的类型可以是任意类型, 但是键必须是可以比较的类型, 如 int, string, float, struct

Go 中没有 set 类型, 可以用 map[T]struct{} 来模拟, struct{} 是一个空结构体, 不占用内存

map 不是并发安全的, 如果需要线程安全, 可以使用 sync.Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 创建
m := make(map[string]int, 10) // 创建一个容量为 10 的映射
m := map[string]int{"a": 1, "b": 2} // 创建并初始化

// 访问
fmt.Println(m["a"]) // 1
fmt.Println(m["c"]) // 0, 不存在的键返回零值
fmt.Println(len(m)) // 2
// 实际上有两个返回值, 第二个返回值表示是否存在
value, exists := m["c"] // 0, false

// 添加
m["c"] = 3

// 删除
delete(m, "c")

// 遍历
for key, value := range m {
fmt.Printf("key: %s, value: %d\n", key, value)
}

// 清空
for key := range m {
delete(m, key)
}
// 或
clear(m) // go1.21+

当键为 math.NaN() 时, 由于 NaN 不等于自身, 所以可以有多个 NaN 键 (其底层由汇编指令 UCOMISD 实现); 应避免用 math.NaN() 作为键

指针

Go 中的指针是一个变量, 其值为另一个变量的地址, 用于存储变量的内存地址; Go 中的指针不能进行运算

1
2
3
4
5
6
7
8
9
10
num := 1
// 获取变量的地址
var pointer *int = &num // 或 pointer := &num
fmt.Println(pointer) // 0xc0000b0008
// 获取指针指向的值
fmt.Println(*pointer) // 1

// 创建一个指向特定类型零值的指针
pointer := new(int)
fmt.Println(*pointer) // 0

new & make

newmake
语法pointer := new(T)slice := make([]T, len, cap)
返回值*TT
参数类型类型, 剩余参数由类型决定
用途给指针分配内存给切片、映射、通道分配内存

结构体

Go 抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go 并非是一个面向对象的语言,但是 Go 依旧有着面向对象的影子,通过结构体和方法也可以模拟出一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义
type Person struct {
Name string
Age int
Hobby []string
secret string // 私有字段
}
type CutePerson struct {
Person // 匿名字段
IsCute bool
}

// 创建
p := Person{"小叶子", 18, []string{"Reading", "Painting"}, "Won't tell you"} // 必须按顺序初始化所有字段
p := Person{
Name: "小叶子",
Age: 18,
} // 未初始化的字段为零值
// 也可以手动编写构造函数 (工厂方法)

// 访问
fmt.Println(p.Name) // 小叶子
fmt.Println(p.secret) // 报错

// 结构体指针会自动解引用
p := &Person{"小叶子", 18, []string{"Reading", "Painting"}, "Won't tell you"}
fmt.Println(p.Name) // 小叶子

函数

Go 中的函数是一等公民, 可以作为参数传递, 也可以作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义
func NameOfFunction(param1 type1, param2 type2) returnType {
// 函数体
return value
}
// 函数表达式
var funcName = func(param1 type1, param2 type2) (returnType1, returnType2) {
// 函数体
return value1, value2
}
// 类型
type FuncType func(param1 type1, param2 type2) returnType

// 给返回值命名
func Add(a, b int) (ans int) {
ans := a + b
return // 等价于 return ans
}

可变参数

1
2
3
4
5
6
7
func sum(args ...int) int {
sum := 0
for _, value := range args {
sum += value
}
return sum
}

匿名函数

匿名函数只能在函数内部定义, 但是可以作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
func(a, b int) {
fmt.Println(a + b)
}(1, 2)

// 回调函数
func addadd(callback func(int, int) int, c int) {
fmt.Println(callback(1, 2) + c)
}
addadd(func(a, b int) int {
return a + b
}, 3)
}

闭包

1
2
3
4
5
6
7
8
9
10
11
12
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
add := adder()
fmt.Println(add(1)) // 1
fmt.Println(add(2)) // 3
}

defer

defer 语句会延迟函数的执行, 直到包含 defer 语句的函数执行完毕后再执行; 通常用于释放资源、关闭文件、解锁等, 可以写在开启任务的后面, 使代码更加清晰

当有多个 defer 语句时, 其执行顺序是后进先出

1
2
3
4
func main() {
defer fmt.Println("World")
fmt.Println("Hello")
}
注意事项

应当避免在 defer 语句中使用使用函数返回值作为参数

1
2
3
4
5
6
7
8
9
10
func main() {
defer fmt.Println(f())
fmt.Println('3')
}
func f() int {
fmt.Println('2')
return '1'
}
// 预期输出: 3 2 1
// 实际输出: 2 3 1

方法

Go 中的方法是一种特殊的函数, 其接收者是一个自定义类型, 可以理解为类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Person struct {
Name string
Age int
}

func (p Person) Say() { // (p Person) 为接收者
fmt.Println("Hello, I'm", p.Name)
}
func (p *Person) Grow() { // 指针接收者, 可以修改接收者的值
p.Age++
}

p := Person{"小叶子", 18}
p.Say() // Hello, I'm 小叶子
p.Grow()
fmt.Println(p.Age) // 19
// 方法只能通过接收者调用, 不能直接调用

函数传参时, 会进行值拷贝, 所以推荐使用指针接收者, 以减少内存开销

接口

Go 中的接口是一种抽象类型, 定义了一组方法, 但是没有具体实现, 只要实现了接口中的所有方法, 就可以称为该接口的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义
type Animal interface {
Eat(string) string
Sleep() string
}

// 实现
type Cat struct {
Name string
}
func (c *Cat) Eat(food string) string {
return c.Name + " is eating " + food
}
func (c *Cat) Sleep() string {
return c.Name + " is sleeping"
}

// 使用
func main() {
// 由于 Cat 实现了 Animal, 所以可以将 Cat 赋值给 Animal
var animal Animal = &Cat{"Tom"}
fmt.Println(animal.Eat("fish")) // Tom is eating fish
fmt.Println(animal.Sleep()) // Tom is sleeping
}

接口是一种隐式实现, 只要实现了接口中的所有方法, 就可以称为该接口的实现, 无需显式 implements

泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 函数参数
func Add[T int | float](a, b T) T {
return a + b
}
fmt.Println(Add(1, 2)) // 自动推断
fmt.Println(Add[float](1.1, 2.2)) // 显式指定

// 切片
type Slice[T any] []T
slice := Slice[int]{1, 2, 3} // 必须显式指定泛型类型

// 映射
type Map[K comparable, V any] map[K]V
m := Map[string, int]{"a": 1, "b": 2}

// 结构体
type Pair[T any] struct {
First, Second T
}

any 表示任意类型, 实质是 interface{} 的别名, comparable 表示可比较的类型; 匿名结构体不支持泛型、匿名函数不支持自定义泛型

类型集

类型集是一种泛型约束, 用于限制泛型类型的范围, 只能用于约束泛型, 不能用作类型实参

1
2
3
4
5
6
7
8
9
10
11
12
13
type SignedInteger interface {
int | int8 | int16 | int32 | int64
}
type UnsignedInteger interface {
uint | uint8 | uint16 | uint32 | uint64
}
type Integer interface {
SignedInteger | UnsignedInteger
}

func Add[T Integer](a, b T) T {
return a + b
}

错误

本段内容

Go 中的错误是一个接口, 只要实现了 Error() string 方法, 就可以称为错误

Go 没有 try catch 语句, 通过返回值来处理错误, 如 if err != nil { return err }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建
import (
"errors"
"fmt"
)
err := errors.New("This is an error")
err := fmt.Errorf("This is an error: %s", "error")
func f() (int, error) {
return 0, errors.New("This is an error")
}
// 为了更好的维护性, 一般会将常用错误定义为全局变量

// 自定义错误
type MyError struct {
Msg string
}
func (e *MyError) Error() string {
return e.Msg
}

panic

panic 用于引发一个运行时错误, 会导致程序崩溃, 但是可以通过 recover 来捕获 panic 引发的错误

程序退出前会执行所有 defer 语句, 所以可以在 defer 语句中使用 recover 来捕获 panic

1
2
3
4
5
6
7
8
9
10
11
info := ""

defer func() {
if err := recover(); err != nil {
fmt.Println("panic error:", err)
}
}()

if info == "" {
panic("info is empty")
}

fatal

fatal 用于引发一个致命错误, 会导致程序崩溃, 不会执行 defer 语句

1
2
3
4
5
6
import "os"

if info == "" {
fmt.Println("info is empty")
os.Exit(1)
}

一般不会主动触发 fatal, 通常是由于系统错误导致

文件

Go 中的二进制数据是以 []byte 的形式存储的 (类似于 JavaScript 中的 Uint8Array)

常用的文件操作可以使用 os 包实现

打开

函数描述
os.Open(name string) (*File, error)打开文件, 只读, 实质是 os.OpenFile(name, os.O_RDONLY, 0)
os.OpenFile(name string, flag int, perm FileMode) (*File, error)打开文件, 可以指定打开方式和权限
os.IsNotExist(err error) bool判断错误是否为文件不存在
os.Lstat(name string) (FileInfo, error)获取文件信息
file.Close() error关闭文件, 通常配合 defer 使用
os.Create(name string) (*File, error)创建文件, 实质是 os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666), 不支持递归创建目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"os"
"fmt"
)

// 打开文件
file, err := os.Open("file.txt")
if os.IsNotExist(err) {
fmt.Println("file not exist")
} else if err != nil {
fmt.Println(err)
} else {
fmt.Println("file opened")
defer file.Close()
}

模式

模式描述
os.O_RDONLY只读
os.O_WRONLY只写
os.O_RDWR读写
os.O_APPEND追加
os.O_CREATE不存在则创建
os.O_TRUNC打开时清空

前三个模式必须指定其一, 后面的按需选择

读取

函数描述
file.Read(p []byte) (n int, err error)读取文件内容到 []byte
os.ReadFile(name string) ([]byte, error)读取文件内容到 []byte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// file.Read
import (
"os"
"fmt"
)

file, _ := os.Open("file.txt")
defer file.Close()

// 定义一个动态扩容的切片逻辑
func ReadText(file *os.File) string {
buf := make([]byte, 0, 1024)
for {
// 如果切片容量不足, 则扩容
if len(buf) == cap(buf) {
buf = append(buf, make([]byte, 1024)...)
}
// 读取文件内容, 直到文件末尾 (超出切片容量的部分会被丢弃)
// n 为读取到的字节数
n, err := file.Read(buf[len(buf):cap(buf)])
// 如果读取到文件末尾, 则退出
if err == io.EOF {
// 从切片中截取有效部分
buf = buf[:len(buf)+n]
break
} else if err != nil {
fmt.Println(err)
break
}
}
return string(buf)
}

fmt.Println(ReadText(file))
1
2
3
4
5
6
7
8
9
10
11
12
// os.ReadFile
import (
"os"
"fmt"
)

data, err := os.ReadFile("file.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(data))
}

写入

函数描述
file.Write(p []byte) (n int, err error)写入 []byte 到文件中
file.WriteString(s string) (n int, err error)写入字符串到文件中
os.WriteFile(name string, data []byte, perm FileMode) error写入 []byte 到文件中
io.WriteString(w Writer, s string) (n int, err error)写入字符串到 Writer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// file.WriteString
import (
"os"
"fmt"
)

file, _ := os.OpenFile("file.txt", os.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC, 0666)
defer file.Close()

for i := 0; i < 10; i++ {
offset, err := file.WriteString("Hello, World!\n")
if err != nil {
fmt.Println(err, offset)
break
}
}
1
2
3
4
5
6
7
8
9
10
11
// os.WriteFile
import (
"os"
"fmt"
)

data := []byte("Hello, World!\n")
err := os.WriteFile("file.txt", data, 0666)
if err != nil {
fmt.Println(err)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.WriteString
import (
"os"
"io"
"fmt"
)

file, _ := os.OpenFile("file.txt", os.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC, 0666)
defer file.Close()

for i := 0; i < 10; i++ {
offset, err := io.WriteString(file, "Hello, World!\n")
if err != nil {
fmt.Println(err, offset)
break
}
}

复制

函数描述
file.ReadFrom(r io.Reader) (n int64, err error)io.Reader 中读取内容到文件中
io.Copy(dst Writer, src Reader) (written int64, err error)复制 ReaderWriter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// file.ReadFrom
import (
"os"
"fmt"
)

src, _ := os.Open("file.txt")
defer src.Close()
dst, _ := os.Create("file_copy.txt")
defer dst.Close()

n, err := dst.ReadFrom(src)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(n)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.Copy
import (
"os"
"io"
"fmt"
)

src, _ := os.Open("file.txt")
defer src.Close()
dst, _ := os.Create("file_copy.txt")
defer dst.Close()

n, err := io.Copy(dst, src)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(n)
}

其他

函数描述
os.Rename(oldpath, newpath string) error移动文件或目录
os.Remove(name string) error删除文件或空目录
os.RemoveAll(name string) error递归删除目录及其子目录
os.ReadDir(name string) ([]DirEntry, error)读取目录内容
file.Readdir(n int) ([]DirEntry, error)n < 0 读取全部, n > 0 读取 n
os.ReadDir 的底层原理
os.Mkdir(name string, perm FileMode) error创建目录
os.MkdirAll(path string, perm FileMode) error递归创建目录
filepath.Walk(dir string, walkFn WalkFunc) error递归遍历目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 封装一个 CopyDir 函数
import (
"os"
"io"
"fmt"
"path/filepath"
)

func CopyDir(src, dst string) error {
// 检查源目录和目标目录状态
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
if !srcInfo.IsDir() {
return fmt.Errorf("%s is not a directory", src)
}
dstInfo, err := os.Stat(dst)
if err != nil {
if os.IsNotExist(err) {
os.MkdirAll(dst, srcInfo.Mode())
} else {
return err
}
} else if !dstInfo.IsDir() {
return fmt.Errorf("%s is not a directory", dst)
}

return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 获取相对路径
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
// 拼接目标路径
dstPath := filepath.Join(dst, relPath)
// 如果是目录, 则创建目录
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
// 如果是文件, 则复制文件
} else {
srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(dstPath)
if err != nil {
return err
}
defer dstFile.Close()
// 复制文件
_, err = io.Copy(dstFile, srcFile)
return err
}
})
}

🚧反射

🚧测试

并发

Go 通过 goroutine 实现并发, goroutine 是一种轻量级的线程, 由 Go 运行时管理; 通过 go 关键字后跟一个函数调用来快速创建一个 goroutine

goroutine 的行为类似于 JavaScript 中的 Promise, 如果不加以控制, 可能会导致程序的主线程提前结束

要控制 goroutine, 可以使用 sync.WaitGroup 来等待所有 goroutine 完成 (类似于 Promise.all); 还可以使用 channel 来进行通信; 以及 context 来控制 goroutine 的生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
"sync"
)

func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println(i)
}(i)
}
wg.Wait()
}

channel

channelGo 中的一种数据结构, 用于在 goroutine 之间传递数据, 是一种线程安全的队列

必须使用 make 创建 channel, channel 有两种类型: unbuffered (同步的) 和 buffered (异步的), 分别对应 make(chan T)make(chan T, n) (其中 n 为缓冲区大小)

对于无缓冲 channel, 发送和接收操作是同步的, 发送操作会阻塞, 直到有其他 goroutine 接收数据; 接收操作也会阻塞, 直到有其他 goroutine 发送数据 (类似于 await)

对于有缓冲 channel, 发送操作不会阻塞, 除非缓冲区满; 接收操作也不会阻塞, 除非缓冲区空

同样可以通过 lencap 函数获取 channel 的长度和容量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
)

func main() {
ch := make(chan int, 1)
defer close(ch) // 关闭 channel

go func() {
ch <- 123 // 将数据发送到 channel
// 此时如果是无缓冲 channel, 则会阻塞
}()

data := <- ch // 从 channel 中接收数据
fmt.Println(data)
}

加解锁操作

通过一个缓冲区为 1channel 来实现加解锁操作

1
2
3
4
5
6
7
8
9
10
11
12
lock := make(chan struct{}, 1)

func FetchData() {
// 加锁, 如果 channel 中有数据, 则会阻塞等待
lock <- struct{}{}
defer func() {
// 解锁
<- lock
}()
// 获取数据
// ...
}

单向管道

channel 可以通过 chan<-<-chan 限制其方向, 分别表示只能发送和只能接收

1
2
3
4
5
6
7
func Send(ch chan<- int) {
ch <- 123
}
func Receive(ch <-chan int) {
data := <-ch
fmt.Println(data)
}

遍历管道

通过 range 关键字可以遍历 channel, 但要记得在发送方适时关闭 channel, 否则会导致死锁

1
2
3
4
5
6
7
8
9
10
11
12
ch := make(chan int, 10)

go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()

for data := range ch {
fmt.Println(data)
}

select

select 语句用于处理多个 channel 的并发操作, 如果没有 case 可执行, 则会阻塞 (除非存在 default)

每个 case 语句必须是一个 channel 操作 (发送或接收), 当满足多个 case 时, 会随机选择一个执行; 当 default 存在时, 如果没有其他 case 可执行, 则会执行 default (而不会再阻塞)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
ch3 := make(chan int)
defer close(ch1)
defer close(ch2)
defer close(ch3)
Send(ch1, 100)
Send(ch2, 200)
Send(ch3, 300)

lock := make(chan struct{}, 1) // 用来控制主线程结束时机

go func() {
Loop:
for {
select {
case data, ok := <-ch1: // ok 为是否处于打开状态
fmt.Println(data, ok)
case data := <-ch2:
fmt.Println(data)
case data := <-ch3:
fmt.Println(data)
case <-time.After(30 * time.Second):
fmt.Println("timeout")
break Loop
}
}
lock <- struct{}{} // 释放主线程
}()

<-lock // 阻塞主线程
}

// 每隔 xxx 毫秒发送一个数据
func Send(ch chan<- int, sleepMS int) {
for {
ch <- 1
time.Sleep(time.Duration(sleepMS) * time.Millisecond)
}
}

WaitGroup

sync.WaitGroup 实质是一个计数器, 用于等待一组 goroutine 完成, 通过 Add 方法增加计数, Done 方法减少计数, Wait 方法等待计数为 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
"sync"
)

func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println(i)
}(i)
}
wg.Wait()
}

context

ContextGo 提供的一种并发控制的解决方案,相比于管道和 WaitGroup,它可以更好的控制子孙协程以及层级更深的协程

Context 本身是一个接口,只要实现了该接口都可以称之为 context, 例如著名 Web 框架 Gin 中的 gin.Context

context 标准库也提供了几个实现,如 emptyCtxcancelCtxtimerCtxvalueCtx

1
2
3
4
5
6
7
8
9
10
type Context interface {
// 返回 上下文的截止时间 和 是否设置了deadline
Deadline() (deadline time.Time, ok bool)
// 返回 上下文是否被取消
Done() <-chan struct{}
// 返回 上下文的错误信息 或 nil
Err() error
// 返回 上下文的值 或 nil
Value(key interface{}) interface{}
}

🚧emptyCtx

🚧valueCtx

🚧cancelCtx

🚧timerCtx

🚧sync

Go 中的 sync 包提供了一些同步原语, 用于控制并发访问

🚧sync.Mutex

🚧sync.RWMutex

🚧sync.Cond

🚧sync.Once

🚧sync.Pool

🚧sync.Map

🚧atomic

⭐Wails

Wails 是一个用于构建桌面应用程序的框架, 使用 GoWeb 技术进行开发, 类似于 RustTauri

1
2
3
4
5
6
7
8
9
# 安装命令行工具
go install github.com/wailsapp/wails/v2/cmd/wails@latest
# 创建项目
wails init -n project-name -t react
wails init -n project-name -t react-ts # 使用 TypeScript
# 运行项目
wails dev
# 打包项目
wails build

调用 Go 函数

Wails 将自动生成 Go 函数的 TypeScript 类型定义, 通过 import { Xxx } from '../wailsjs/go/main/App' 即可引入; 所有的 Go 函数都返回 Promise

1
2
3
4
5
6
7
8
9
10
11
import { SayHello } from '../wailsjs/go/main/App'

export default function App() {
const handleClick = async () => {
const res = await SayHello('小叶子')
console.log(res)
}
return (
<button onClick={handleClick}>Click Me</button>
)
}

APIs

Go 中的 API 通过导入 github.com/wailsapp/wails/v2/pkg/runtime 来获取, 所有函数的第一个参数 context 都是应用启动时传入的上下文

JavaScript 中的 API 全部挂在 window.runtime 下, 可以通过 runtime.xxx 来调用

GoJavaScript描述
Hide(c)runtime.Hide()隐藏窗口
Show(c)runtime.Show()显示窗口
Quit(c)runtime.Quit()退出应用
BrowserOpenURL(c, "url")runtime.BrowserOpenURL('url')在默认浏览器中打开链接
ClipboardGetText(c)runtime.ClipboardGetText()获取剪贴板文本
ClipboardSetText(c, "text")runtime.ClipboardSetText('text')设置剪贴板文本
MessageDialog(c, MessageDialogOptions)消息对话框, 返回 (string, error)

MessageDialogOptions

字段描述
Type弹窗类型, InfoDialogErrorDialogWarningDialogQuestionDialog
Title标题
Message消息
Buttons按钮, 仅对 Mac 有效
DefaultButton默认按钮, OKCancelYesNo
CancelButton取消按钮, OKCancelYesNo

事件

Wails 中的事件在 GoJavaScript 之间是统一的

GoJavaScript描述
EventsOn(c, "event", f([data]))runtime.EventsOn('event', f([data]))监听事件
EventsOff(c, "event")runtime.EventsOff('event')取消监听事件
EventsOnce(c, "event", f([data]))runtime.EventsOnce('event', f([data]))一次性监听事件
EventsOnMultiple(c, "event", f([data]), count)runtime.EventsOnMultiple('event', f([data]), count)监听多次事件, 返回取消监听的函数
EventsEmit(c, "event", data)runtime.EventsEmit('event', data)触发事件

窗口

GoJavaScript描述
WindowSetTitle(c, "title")runtime.WindowSetTitle('title')设置窗口标题
WindowFullscreen(c)runtime.WindowFullscreen()全屏窗口
WindowUnFullscreen(c)runtime.WindowUnFullscreen()退出全屏
WindowIsFullscreen(c)runtime.WindowIsFullscreen()判断是否全屏
WindowCenter(c)runtime.WindowCenter()居中窗口
WindowReload(c)runtime.WindowReload()重新加载窗口
WindowSetAlwaysOnTop(c, bool)runtime.WindowSetAlwaysOnTop(bool)设置窗口是否置顶
WindowMaximise(c)
WindowUnmaximise(c)
WindowIsMaximised(c)
runtime.WindowMaximise()
runtime.WindowUnMaximise()
runtime.WindowIsMaximised()
最大化窗口
WindowMinimise(c)
WindowUnminimise(c)
WindowIsMinimised(c)
runtime.WindowMinimise()
runtime.WindowUnMinimise()
runtime.WindowIsMinimised()
最小化窗口
WindowToggleMaximise(c)runtime.WindowToggleMaximise()在最大化和非最大化之间切换

配置

wails.Run() 方法接收一个 options.App 结构体, 用于配置应用

字段类型描述
Widthint窗口宽度
Heightint窗口高度
Titlestring窗口标题
Framelssbool是否无边框
MinWidthint窗口最小宽度
MinHeightint窗口最小高度
MaxWidthint窗口最大宽度
MaxHeightint窗口最大高度
StartHiddenbool启动时是否隐藏
BackgroundColour*options.RGBA背景颜色
AlwaysOnTopbool是否置顶
OnStartupfunc(c)启动时 (index.html 加载前) 回调
OnDomReadyfunc(c)DOM 加载完成后回调
OnShutdownfunc(c)关闭时回调
OnBeforeClosefunc(c)关闭回调
Windows*windows.OptionsWindows 配置
Mac*mac.OptionsMac 配置
Linux*linux.OptionsLinux 配置

对于无边框窗口, Wails 提供了一个非常简单的拖动解决方案: 任何具有 --wails-draggable:drag 样式的元素都可以拖动窗口

Windows

字段类型描述
WebviewIsTransparentboolWebview 是否透明
WindowIsTranslucentbool窗口是否半透明
BackdropTypewindows.BackdropType半透明背景类型
DisableWindowIconbool禁用窗口图标

半透明背景类型有 3: Acrylic (亚克力) 和 2: Mica (亚克力玻璃) 等

Mac

字段类型描述
TitleBar*mac.TitleBar标题栏外观
WebviewIsTransparentboolWebview 是否透明
WindowIsTranslucentbool窗口是否半透明

Linux

字段类型描述
WindowIsTranslucentbool窗口是否半透明

Github Action

通过设置 Github Action 可以实现在创建 Tag 时自动构建并发布应用; 以下是官方提供的 Wails 构建 Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
name: Wails build

on:
push:
tags:
# Match any new tag
- '*'

env:
# Necessary for most environments as build failure can occur due to OOM issues
NODE_OPTIONS: "--max-old-space-size=4096"

jobs:
build:
strategy:
# Failure in one platform build won't impact the others
fail-fast: false
matrix:
build:
- name: 'App'
platform: 'linux/amd64'
os: 'ubuntu-latest'
- name: 'App'
platform: 'windows/amd64'
os: 'windows-latest'
- name: 'App'
platform: 'darwin/universal'
os: 'macos-latest'

runs-on: ${{ matrix.build.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: recursive

- name: Build wails
uses: dAppServer/[email protected]
id: build
with:
build-name: ${{ matrix.build.name }}
build-platform: ${{ matrix.build.platform }}
package: false
go-version: '1.20'

⭐Playwright

Playwright 是一个用于自动化测试的工具, 支持 JavaScript, Python, Go 等多种语言

1
2
3
4
5
# 安装
go get -u github.com/playwright-community/playwright-go
# 安装浏览器
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps
# 也可以在应用内安装
1
err := playwright.Install()

🚧Page

🚧Locator

🚧Frame

  • 标题: GoLang/Wails学习笔记
  • 作者: 小叶子
  • 创建于 : 2024-05-02 12:00:00
  • 更新于 : 2024-05-20 15:13:42
  • 链接: https://blog.leafyee.xyz/2024/05/02/Go学习笔记/
  • 版权声明: 版权所有 © 小叶子,禁止转载。
评论