type
status
date
slug
summary
tags
category
icon
password
完成了一个todoList用来快速掌握Go语言的语法
比如读取的这个地方用的:
bufio.NewReader(os.Stdin)
- bufio 是 Go 标准库中的一个包,用于提供缓冲 I/O 操作。
- os.Stdin 是标准输入(通常是键盘)。
- NewReader 创建一个从标准输入读取的缓冲读取器。
reader.ReadString('\n')
- ReadString 是 bufio.Reader 的一个方法。
- 它读取输入直到遇到指定的分隔符(这里是 '\n',即换行符)。
- 返回读取的字符串和一个错误(这里用 _ 忽略了错误)
_ 是空白标识符,用于忽略不需要的返回值(这里是错误)—》可以留意这个错误处理的方式,和java不一样
检查切片是否为空,注意这里用的是len就是实际的长度,不是cap了,区分一下。
for _,todo 因为这里没有用i来做索引,就是遍历todo所以才用的 _ 来处理
- 声明一个整型变量 id。
- fmt.Scanln(&id) 从标准输入读取一个整数。
- &id 是 id 的内存地址,允许 Scanln 直接修改 id 的值。
- _ 忽略 Scanln 返回的扫描项数。
- err 接收可能的错误。
理解一下这块的指针:
先回顾一下指针的概念,太久没接触过指针,这块已经有点忘了。
类比:邮政地址
想象一下,变量就像是房子,而指针就像是这个房子的地址:1. 房子(变量):存储实际的东西(数据)。
- 地址(指针):告诉你房子在哪里(内存地址)。
基本概念:
- 变量:存储数据的地方。
- 指针:存储另一个变量的内存地址。
拿一个样例来理解更好:
- x := 10: 创建一个整数变量 x 并赋值 10。
- var p *int = &x:
&x 获取 x 的内存地址。
p 是一个指针变量,存储 x 的地址。
- p = 20:
p 表示"p 指向的值"。
这行代码通过指针修改 x 的值。
- changeValue(&x):
传递 x 的地址给函数。
函数可以直接修改 x 的值。
& 符号(取地址操作符):
用在变量前面
意思是"获取这个变量的内存地址"
例如:&x 表示 x 的内存地址
符号有两种用途:
a. 声明指针类型:
用在类型前面
例如:var p *int 声明 p 为整数指针
b. 解引用操作符:
用在指针变量前面
意思是"获取这个地址上存储的值"
例如:*p 表示 p 指向的值
复杂一点的版本
基于上述版本,加深一些复杂度,以此可以帮助理解更多go的特性。
这里主要是封装
和上面相比
这里要理解一个事情 就是这个函数的为什么可以这么写?(tl *TodoList)
对比理解:就是java类中的方法的意思
定义方式:
- Go 在结构体外部定义方法,但通过接收者与结构体关联。
- Java 在类的内部直接定义方法。
- 接收者 vs this:
- Go 使用显式的接收者(如 (p Person) 或 (p *Person))。
- Java 隐式使用 this 关键字引用当前对象。
- 指针接收者 vs 引用:
- Go 的指针接收者(如 (p *Person))类似于 Java 中通过引用修改对象状态。
- Java 默认通过引用传递对象,不需要显式指定。
- 方法调用:
- 两种语言的方法调用语法非常相似。
- 封装:
- Go 通过大小写控制可见性(大写开头为公开)。
- Java 使用 public、private 等关键字控制可见性
然后这里用上* 指针接收,就是类似于c那种,可以对结构体的东西进行修改,所以就是要这样做。
举个例子:
最终效果一样。只是过程用了封装
主要是学会用* 来修改结构体
错误处理
意思是如果传空的,可能返回error类型的。没问题的就直接nil,就是没错的意思
fmt.Errorf 是 Go 标准库 fmt 包中的一个函数,用于创建格式化的错误消息。它的工作原理类似于 fmt.Sprintf,但是它返回一个 error 类型而不是字符串。
错误处理,主要是为了定位问题,保持函数是这样的格式就好,没事就nil,有事Errorf
接口
接口定义好方法,注意大写就是public的,其他可以用。然后对应的方法也要大写。
可以看看这里的用法,其实和用结构体差不多,
- 大写开头的标识符(如 TodoManager)是公开的(public),可以被其他包导入和使用。
- 小写开头的标识符是私有的(private),只能在定义它的包内部使用。
对比一下结构体和接口:
定义和用途:
结构体(Struct):定义数据结构,包含字段。
接口(Interface):定义方法集,不包含数据。
实现:
结构体:直接定义字段和方法。
接口:只定义方法签名,由其他类型(如结构体)来实现。
数据存储:
结构体:可以存储数据。
接口:不存储数据,只是一个抽象定义。
实例化:
结构体:可以直接实例化。
接口:不能直接实例化,只能声明接口类型的变量,并赋予实现了该接口的值。
灵活性:
结构体:相对固定,一旦定义就不容易改变。
接口:非常灵活,任何类型只要实现了接口定义的所有方法,就被认为实现了该接口。
多态性:
结构体:本身不提供多态性。
接口:是 Go 实现多态的主要方式。
文件持久化
用这个json Marshal/ Unmarshal 来序列化和反序列化 写入
直接用os.WirteFile就能写入了
然后就是说要导出的一定要加上json的,然后要大写,不然没办法导出去的,我指的是结构那里。还是遵循原则就是 大小写来表示public or not
并发处理
先从最基本的例子来跑,以此来理解并发
go就是并发的关键字
time.Sleep(1 * time.Second)这里这样写的意思是:
- Go 程序在主函数返回时会退出,即使还有其他 goroutines 在运行。
- 如果没有这行代码,主函数可能会在生产者和消费者完成工作之前就结束,导致程序过早退出。
然后就是生产者和消费者那两个函数的入参,意思就是说:
- chan<- T:只能发送 T 类型的值(只写 channel)
- <-chan T:只能接收 T 类型的值(只读 channel)
箭头指向就代表了是入 还是 出
使用 &wg 来传递 WaitGroup 的地址有几个重要原因:
传递引用而非副本
如果我们不使用指针,每个 worker 函数会得到 WaitGroup 的一个副本,而不是原始的 WaitGroup。
我们希望所有的 goroutine 操作同一个 WaitGroup 实例。
使用指针确保每个 worker 都在修改同一个 WaitGroup 的计数。
select语句就是运行响应优先准备好的channel。
<-ch1 就是比如msg1接收的意思,有了就处理。
在 NewTodoList 函数中,创建了一个带缓冲的 channel saveChan。
同时启动了一个 goroutine 运行 saveWorker 方法。这个 goroutine 会一直运行,等待 saveChan 中的信号。
每当需要保存数据时(如添加、完成或删除任务后),会调用 Save 方法。
Save 方法尝试向 saveChan 发送一个信号(空结构体)。如果 channel 已满,它会立即返回而不阻塞。
当 saveWorker 从 channel 接收到信号时,它会调用 saveToFile 方法来实际保存数据。
这种设计的优点是:
保存操作是异步的,不会阻塞主程序的执行。
如果短时间内多次调用 Save,只会触发一次实际的文件写入操作,避免频繁 I/O。