1
2
3
4
5
6
7
8
9
10
11
12
13
whatAmI := func(i interface{}) {
switch t := i.(type) {
case bool:
fmt.Println("I'm a bool")
case int:
fmt.Println("I'm an int")
default:
fmt.Printf("Don't know type %T\n", t)
}
}
whatAmI(true)
whatAmI(1)
whatAmI("hey")

这是一个函数whatAmI,它接受一个interface{}类型的参数。interface{}是 Go 语言中的一种空接口,可以存储任何类型的值。
这个switch语句实际上是一个类型switch,它检查接口变量i的具体类型,而不是它的值。根据i的类型,switch将执行不同的case分支。

1
2
3
4
// If you specify the index with `:`, the elements in
// between will be zeroed.
b = [...]int{100, 3: 400, 500}
fmt.Println("idx:", b)

3: 400 指定了下标为 3 的位置的值是 400。
这意味着索引 1 和 2 的值将被初始化为 0(因为:操作符指定了一个新的下标,并初始化之前未指定的下标对应的元素为 0)。

1
2
var s []string
fmt.Println("uninit:", s, s == nil, len(s) == 0)

uninit: [] true true

1
2
3
4
5
6
7
8
9
10
twoD := make([][]int, 3)
for i := 0; i < 3; i++ {
innerLen := i + 1
twoD[i] = make([]int, innerLen)
for j := 0; j < innerLen; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}

2d: [[0] [1 2] [2 3 4]]

12.3日

map操作:

1
2
3
4
5
6
7
8
9
10
11
12
   m["k1"] = 7
m["k2"] = 13
fmt.Println("map:", m)
fmt.Println("len:", len(m))
//clear(m)
val, prs := m["k2"] //k2的值, 一个是判断k2是否存在于m中的bool值
fmt.Println("val:", val, "prs:", prs)
n := map[string]int{"foo": 1, "bar": 2}
fmt.Println("", n)
if maps.Equal(n, n2) {
fmt.Println("n == n2")
}

使用变长参数函数(variadic function),即可以接收任意数量参数的函数。
这里的 … 标记表示 nums 参数是可变长的,可以接收零个或多个 int 类型的参数

nums 被当作切片使用,可以进行切片相关的操作,例如使用 len(nums) 获取长度,或者用 range 遍历切片元素。

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
func sum(nums ...int) {
fmt.Print(nums, " ")
total := 0
// Within the function, the type of `nums` is
// equivalent to `[]int`. We can call `len(nums)`,
// iterate over it with `range`, etc.
for _, num := range nums {
total += num
}
fmt.Println(total)
}

func main() {

// Variadic functions can be called in the usual way
// with individual arguments.
sum(1)
sum(1, 2)
sum(1, 2, 3)

// If you already have multiple args in a slice,
// apply them to a variadic function using
// `func(slice...)` like this.

//第二种调用nums的方法使用 nums 切片,通过 sum(nums...) 的方式将切片中的每个元素作为单独的参数传递给 sum 函数。
nums := []int{1, 2, 3, 4}
sum(nums...)
}

错误示范

1
2
3
4
5
6
7
8
9
10
11
12

func allArticles(w http.ResponseWriter, r *http.Request) {
articles := Articles{
Title: "Test Title",
Desc: "Test Descrption",
Content: "hello world",
},

fmt.Println("endpoint hit: allArticles")
json.NewEncoder(w).Encode(articles)
}

这里的问题是 Articles 类型实际上是一个切片([]Article),而你试图将一个单独的 Article 结构体实例赋值给 articles 变量。根据 Go 语言的语法,Articles 类型需要的是一个 Article 类型的切片。

要解决这个问题,你需要将 Article 实例放入一个切片中。可以将你的代码修改为:

1
2
3
4
5
6
7
8
9
10
11
12
articles := Articles{
{
Title: "Test Title",
Desc: "Test Descrption",
Content: "hello world",
},
}
CopyInsert
这里的修改是把 Article 结构体实例用 {} 包围起来,表示这是一个切片中的一个元素。

最终的 allArticles 函数应如下所示:

第二种方法:

1
2
3
4
5
6
7
8
9
// 创建Article的实例
newArticle := Article{
Title: "Test Title",
Desc: "Test Description",
Content: "hello world",
}

// 创建Articles切片并添加上面的实例
articles := Articles{newArticle}

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
2
3
4
5
6
7
func intSeq() func() int {
i := 0 // i 是一个局部变量,用于 intSeq 函数。
return func() int { // 返回一个匿名函数,这个函数可以访问 intSeq 函数的局部变量 i。
i++ // 每次调用这个匿名函数时,i 的值都会增加。
return i // 返回增加后的 i 的值。
}
}

返回结果 :
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.txtfile2.txt 被传递给程序作为命令行参数。
  • 在程序中,os.Args 是一个字符串切片([]string),包含了命令行中的参数:

    1
    2
    3
    os.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
2
3
4
5
6
7
8
9
for _, arg := range files {
f, err := os.Open(arg) // 打开文件
if err != nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err) // 错误处理
continue
}
defer f.Close() // 确保文件被正确关闭
countLines(f, counts)
}

这段代码中:

  1. **os.Open(arg)**:

    • 打开文件(如 file1.txt),返回一个文件指针 *os.File
    • 如果文件打开失败(文件不存在或权限问题),返回错误并跳过。
  2. **countLines(f, counts)**:

    • 将文件指针传递给 countLines,扫描文件的内容并统计每行的出现次数。

3. 执行结果的输出

统计完成后,程序会输出所有出现次数大于 1 的行:

1
2
3
4
5
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}

这会将文件中所有重复的行及其出现次数打印到控制台。


4. 为什么能够读取多个文件

这是因为你的代码通过 for _, arg := files 遍历了 os.Args[1:] 中的所有文件,并依次处理它们。程序对每个文件重复以下流程:

  1. 打开文件。
  2. 按行扫描内容,统计出现次数。
  3. 将结果合并到 counts 映射中。

这样,即使你提供多个文件,程序依然可以逐个读取并处理它们。


运行流程示例

假设有以下文件内容:

file1.txt:

1
2
3
hello
world
hello

file2.txt:

1
2
3
world
hello
go

执行命令:

1
go run main.go file1.txt file2.txt

程序会:

  1. 读取 file1.txt
    • 统计结果:{"hello": 2, "world": 1}
  2. 读取 file2.txt,继续更新 counts
    • 最终统计结果:{"hello": 3, "world": 2, "go": 1}
  3. 打印重复行:
    1
    2
    3   hello
    2 world

总结

你可以读取多个文件是因为程序通过 os.Args 获取了命令行中的文件名列表,并通过循环逐个打开和读取文件的内容。这种设计使得你的程序能够动态处理任意数量的文件,而无需修改代码。


12.14 日 更考完4级

一.Errors

  1. “errors” 包 使用给定的错误消息构造一个基本错误值。

errors.New(“can’t work with 42”)

nil 表示没有错误

  1. 哨兵错误是一个预先声明的变量,用于表示特定的错误情况。

var ErrOutOfTea = fmt.Errorf(“no more tea available”)
var ErrPower = fmt.Errorf(“can’t boil water”)

  1. 更高级别的错误来包装错误以添加上下文

最简单的方法是使用 fmt.Errorf 中的 %w 动词。包装错误创建一个逻辑链(A 包装 B,B 包装 C 等),可以使用 error.Is 和 error.As 等函数进行查询。

return fmt.Errorf(“making tea: %w”, ErrPower)

  1. if 行中使用内联错误检查

if r, e := f(i); e != nil {
fmt.Println(“f failed:”, e)
} else {
fmt.Println(“f worked:”, r)
}

  1. 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
2
3
4
5
6
7
8
9
10
type argError struct {
arg int
message string
}

func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.message)
}


Error 方法并没有直接调用,但 &argError{arg, “can’t work with it”} 被返回为 error 类型时,Error 方法已经在幕后实现了对错误消息的封装。

error.As 是errors.Is 的更高级版本。它检查给定错误(或其链中的任何错误)是否与特定错误类型匹配,并转换为该类型的值,返回 true。如果没有匹配,则返回 false。

Goroutines

轻量级的执行线程

go f(s)。这个新的 goroutine 将与调用的 goroutine 同时执行。

1
2
3
4
5
6
7
8
func f(from string) {
for i := 0; i < 3; i++ {
fmt.Println(from, ":", i)
}
}

go f("goroutine")

启动一个 goroutine 来进行匿名函数调用。

1
2
3
go func(msg string) {
fmt.Println(msg)
}("going")

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
2
3
4
5
6
7
8
 messages := make(chan string, 2)

messages <- "buffered"
messages <- "channel"

fmt.Println(<-messages)
fmt.Println(<-messages)

断言和switch用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

类型断言用于从接口类型值中提取其底层的具体类型。如果断言成功,程序可以安全地将该接口值转换为目标类型并使用。

value, ok := x.(T)

x 是一个接口类型的变量。
T 是目标类型(可以是具体类型或其他接口类型)。
如果 x 的动态类型是 T:
value 是 x 转换为 T 后的值。
ok 为 true。
如果 x 的动态类型不是 T:
value 是 T 的零值。
ok 为 false,表示断言失败。

1
2
3
4
5
6
7
8
var x interface{} = 42 // 空接口存储了一个 int 值

value, ok := x.(int) // 断言 x 是 int 类型
if ok {
fmt.Printf("x is an int, value: %d\n", value)
} else {
fmt.Println("x is not an int")
}

空interface
空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。

1
2
3
4
5
6
7
// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s

一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊!

Go并发