Go语法学习
1 | whatAmI := func(i interface{}) { |
这是一个函数whatAmI,它接受一个interface{}类型的参数。interface{}是 Go 语言中的一种空接口,可以存储任何类型的值。
这个switch语句实际上是一个类型switch,它检查接口变量i的具体类型,而不是它的值。根据i的类型,switch将执行不同的case分支。
1 | // If you specify the index with `:`, the elements in |
3: 400 指定了下标为 3 的位置的值是 400。
这意味着索引 1 和 2 的值将被初始化为 0(因为:操作符指定了一个新的下标,并初始化之前未指定的下标对应的元素为 0)。
1 | var s []string |
uninit: [] true true
1 | twoD := make([][]int, 3) |
2d: [[0] [1 2] [2 3 4]]
12.3日
map操作:
1 | m["k1"] = 7 |
使用变长参数函数(variadic function),即可以接收任意数量参数的函数。
这里的 … 标记表示 nums 参数是可变长的,可以接收零个或多个 int 类型的参数
nums 被当作切片使用,可以进行切片相关的操作,例如使用 len(nums) 获取长度,或者用 range 遍历切片元素。
1 | func sum(nums ...int) { |
错误示范
1 |
|
这里的问题是 Articles 类型实际上是一个切片([]Article),而你试图将一个单独的 Article 结构体实例赋值给 articles 变量。根据 Go 语言的语法,Articles 类型需要的是一个 Article 类型的切片。
要解决这个问题,你需要将 Article 实例放入一个切片中。可以将你的代码修改为:
1 | articles := Articles{ |
第二种方法:
1 | // 创建Article的实例 |
go fmt
包的导入者将使用名称来引用其内容,因此包中的导出名称可以使用该事实来避免重复。 (不要使用 import . 表示法,它可以简化必须在正在测试的包外部运行的测试,但应该避免。)
例如,bufio 包中的缓冲读取器类型称为 Reader,而不是 BufReader,因为用户将其视为 bufio.Reader,这是一个清晰、简洁的名称。此外,由于导入的实体始终使用其包名称进行寻址,
因此 bufio.Reader 不会与 io.Reader 冲突。类似地,
创建ring.Ring新实例的函数(Go中构造函数的定义)通常被称为NewRing,但由于Ring是包导出的唯一类型,并且由于包被称为ring,所以它是称为“New”,包的客户端将其视为“ring.New”。使用包结构来帮助您选择好的名称。
另一个简短的例子是once.Do; Once.Do(setup) 读起来很好,
并且不会通过编写 Once.DoOrWaitUntilDone(setup) 来改进。长名称不会自动使内容更具可读性。有用的文档注释通常比超长的名称更有价值。
闭包
1 | func intSeq() func() int { |
返回结果 :
1
2
3
1
这是因为闭包允许匿名函数捕获并记住 i 变量的状态。
这就是闭包的 “魔力” 所在,即使外部函数已经返回,内部函数仍然能够访问和修改外部函数的变量。
stack := []rune{} 与 make := []string{} 的区别:
stack := []rune{}:这段代码创建了一个 rune 类型的切片(空切片)。rune 是 Go 中专门用来表示单个字符的类型,适用于你处理字符(尤其是 Unicode 字符)时,能够确保每个元素是一个完整的 Unicode 字符。
make := []string{}:这段代码创建了一个 string 类型的切片。每个元素是一个字符串,字符串通常用于表示一系列字符构成的文本数据。
在 Go 程序中,通过 os.Args
可以访问命令行参数,而 go run main.go file1.txt file2.txt
的执行过程如下:
为什么·go语言 > go run main.go file1.txt file2.txt 在这里执行之后可以读取到文件1 2
1. 命令行参数的解析
命令:
go run main.go file1.txt file2.txt
go run
会编译并执行main.go
。- 命令行中,
main.go
后面的file1.txt
和file2.txt
被传递给程序作为命令行参数。
在程序中,
os.Args
是一个字符串切片([]string
),包含了命令行中的参数:1
2
3os.Args[0] // 包含程序的名字,如 "main.go"
os.Args[1] // 第一个参数 "file1.txt"
os.Args[2] // 第二个参数 "file2.txt"例子:
假如命令为:1
go run main.go file1.txt file2.txt
则:
1
fmt.Println(os.Args)
输出:
1
[main file1.txt file2.txt]
2. 打开文件进行读取
你的代码解析命令行参数后,会尝试打开这些文件:
1 | files := os.Args[1:] // 获取命令行参数列表(除程序本身外的部分) |
files
现在是一个切片:["file1.txt", "file2.txt"]
。
然后通过循环处理每个文件:
1 | for _, arg := range files { |
这段代码中:
**
os.Open(arg)
**:- 打开文件(如
file1.txt
),返回一个文件指针*os.File
。 - 如果文件打开失败(文件不存在或权限问题),返回错误并跳过。
- 打开文件(如
**
countLines(f, counts)
**:- 将文件指针传递给
countLines
,扫描文件的内容并统计每行的出现次数。
- 将文件指针传递给
3. 执行结果的输出
统计完成后,程序会输出所有出现次数大于 1 的行:
1 | for line, n := range counts { |
这会将文件中所有重复的行及其出现次数打印到控制台。
4. 为什么能够读取多个文件
这是因为你的代码通过 for _, arg := files
遍历了 os.Args[1:]
中的所有文件,并依次处理它们。程序对每个文件重复以下流程:
- 打开文件。
- 按行扫描内容,统计出现次数。
- 将结果合并到
counts
映射中。
这样,即使你提供多个文件,程序依然可以逐个读取并处理它们。
运行流程示例
假设有以下文件内容:
file1.txt
:
1 | hello |
file2.txt
:
1 | world |
执行命令:
1 | go run main.go file1.txt file2.txt |
程序会:
- 读取
file1.txt
:- 统计结果:
{"hello": 2, "world": 1}
。
- 统计结果:
- 读取
file2.txt
,继续更新counts
:- 最终统计结果:
{"hello": 3, "world": 2, "go": 1}
。
- 最终统计结果:
- 打印重复行:
1
23 hello
2 world
总结
你可以读取多个文件是因为程序通过 os.Args
获取了命令行中的文件名列表,并通过循环逐个打开和读取文件的内容。这种设计使得你的程序能够动态处理任意数量的文件,而无需修改代码。
12.14 日 更考完4级
一.Errors
- “errors” 包 使用给定的错误消息构造一个基本错误值。
errors.New(“can’t work with 42”)
nil 表示没有错误
- 哨兵错误是一个预先声明的变量,用于表示特定的错误情况。
var ErrOutOfTea = fmt.Errorf(“no more tea available”)
var ErrPower = fmt.Errorf(“can’t boil water”)
- 更高级别的错误来包装错误以添加上下文
最简单的方法是使用 fmt.Errorf 中的 %w 动词。包装错误创建一个逻辑链(A 包装 B,B 包装 C 等),可以使用 error.Is 和 error.As 等函数进行查询。
return fmt.Errorf(“making tea: %w”, ErrPower)
- if 行中使用内联错误检查
if r, e := f(i); e != nil {
fmt.Println(“f failed:”, e)
} else {
fmt.Println(“f worked:”, r)
}
- error.Is 检查给定错误(或其链中的任何错误)是否与特定错误值匹配。
if errors.Is(err, ErrOutOfTea) {
fmt.Println(“We should buy new tea!”)
} else if errors.Is(err, ErrPower) {
fmt.Println(“Now it is dark.”)
} else {
fmt.Printf(“unknown error: %s\n”, err)
}
将err和我们自定义的哨兵错误或者特定错误做对比
二.自定义错误
1 | type argError struct { |
Error 方法并没有直接调用,但 &argError{arg, “can’t work with it”} 被返回为 error 类型时,Error 方法已经在幕后实现了对错误消息的封装。
error.As 是errors.Is 的更高级版本。它检查给定错误(或其链中的任何错误)是否与特定错误类型匹配,并转换为该类型的值,返回 true。如果没有匹配,则返回 false。
Goroutines
轻量级的执行线程
go f(s)。这个新的 goroutine 将与调用的 goroutine 同时执行。
1 | func f(from string) { |
启动一个 goroutine 来进行匿名函数调用。
1 | go func(msg string) { |
goroutine 是由 Go 运行时同时运行的。
通道
通道是连接并发 goroutine 的管道。您可以将值从一个 Goroutine 发送到通道,并将这些值接收到另一个 Goroutine。
messages := make(chan string)
通道 <- 语法将值发送到通道
go func() { messages <- “ping” }()
msg := <-messages
<-channel 语法从通道接收值。在这里,我们将收到上面发送的“ping”消息并将其打印出来。
发送和接收会阻塞,直到发送者和接收者都准备好为止。此属性允许我们在程序结束时等待“ping”消息,而无需使用任何其他同步。
通道缓冲:
messages := make(chan string, 2) 字符串通道,最多缓冲 2 个值 因此我们可以将这些值发送到通道中,而无需相应的并发接收。
1 | messages := make(chan string, 2) |
断言和switch用法:
1 | var t interface{} |
类型断言用于从接口类型值中提取其底层的具体类型。如果断言成功,程序可以安全地将该接口值转换为目标类型并使用。
value, ok := x.(T)
x 是一个接口类型的变量。
T 是目标类型(可以是具体类型或其他接口类型)。
如果 x 的动态类型是 T:
value 是 x 转换为 T 后的值。
ok 为 true。
如果 x 的动态类型不是 T:
value 是 T 的零值。
ok 为 false,表示断言失败。
1 | var x interface{} = 42 // 空接口存储了一个 int 值 |
空interface
空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。
1 | // 定义a为空接口 |
一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊!
Go并发