Hi there 👋

Take my broken heart, make it into art.

使用 vscode + deepseek 代替 cursor

使用 vscode + deepseek 代替 cursor

January 18, 2025 · 1 min · Max

由 append 引发的一个疑问

由 append 引发的一个疑问 我们先来看一个问题 package main import "fmt" func main() { a := make([]int, 0, 5) AddElm(a, 5) fmt.Println(a) } func AddElm(a []int, i int) { a = append(a, i) } 上面这一段代码中,a 这个切片会输出什么呢?可以试着运行一下 答案是空切片,为什么呢?首先我们知道 append 这个操作,在容量足够的情况下是不会新生成一个 slice 来进行扩容的,所以这里排除这种情况。 已知这个 slice 也是值传递的方式传入的,那么是不是因为他 copy 了一份数据到函数内部从而导致的函数内外不一致的情况呢?我们打印地址看一下 package main import "fmt" func main() { a := make([]int, 0, 5) fmt.Printf("before: %p\n", a) AddElm(a, 5) fmt.Printf("after: %p\n", a) fmt.Println(a) } func AddElm(a []int, i int) { a = append(a, i) fmt.Printf("under: %p\n", a) } // 输出如下: // before: 0xc0000a8030 // under: 0xc0000a8030 // after: 0xc0000a8030 // [] 可以发现,在申明开始-函数内-函数追加赋值结束时打印,这三个地址都是一样的。 ...

February 20, 2022 · 3 min · Max

golang 中提防竞态条件的产生

我们来看下面这段代码: package main import ( "time" ) type testInt struct { Num int } func main() { a := make([]int, 0) for i := 1; i < 10; i++ { a = append(a, i) } t := testInt{Num: 1} for _, v := range a { go func(i int) { t.Num = i pTs(t) }(v) } time.Sleep(time.Second*3) } func pTs(t testInt) { println(t.Num) } 你可以复制出来执行一下,会不会有问题呢?从执行的结果来看,是没问题的。 但是如果你要加上 -race 参数去执行,就会发现在 t.Num = i 这里报错了,这是为什么呢? 这就要说到一个 go 语言的编程常识了(可能不限语言),就是在多个协程去处理一个变量的时候记得要加锁或者用 atomic 进行原子操作,不管结果是否正常。因为如果不这么做,有天出了问题可是不好排查的。 建议大家拿到陌生代码,都 -race 去检测一下,就是这样。 对于为什么不加锁执行起来也没有问题,我还没有想明白,希望有大明白能指点一二。

February 9, 2022 · 1 min · Max

控制 goroutine 执行顺序的一例

依次打印 题目:依次输出 dog、pig、sheep,并执行 100 次,每个输出都需要一个单独的 goroutine 这里我们通过有缓冲的容量为 1 的 channel 来标记状态,通过 for-select 结构来控制当前要输出哪一个单词,因为 select 在 channel 阻塞的时候不会执行对应的 case,我们可以通过这个特性来操作。 具体思路如下: 设置三个 channel,初始化时使 dog 的 channel 中有一条数据,这样 select 就会先输出 dog 然后在 dog 的 case 中结束的时候给 pig 的 channel 中新增一条数据,这样下次就会进入 pig 的 case 进行输出操作 过程中我们可以使用 atomic 包来控制次数,不会产生并发问题 具体代码如下: package main import ( "fmt" "sync" "sync/atomic" ) var wg sync.WaitGroup var counter uint64 = 0 var dogChan = make(chan struct{}, 1) var pigChan = make(chan struct{}, 1) var sheepChan = make(chan struct{}, 1) func main() { wg.Add(1) go animalPrint() dogChan <- struct{}{} wg.Wait() } func animalPrint() { for { if counter == uint64(300) { wg.Done() return } select { case <-dogChan: fmt.Println("dog") atomic.AddUint64(&counter, 1) pigChan <- struct{}{} case <-pigChan: fmt.Println("pig") atomic.AddUint64(&counter, 1) sheepChan <- struct{}{} case <-sheepChan: fmt.Println("sheep") atomic.AddUint64(&counter, 1) dogChan <- struct{}{} } } } 是不是觉得重复代码有点多? 我们还是依照上面的思路,把代码换种方式实现: ...

February 8, 2022 · 2 min · Max

推荐两个学习 go 的网站

一个就是 exercism, 这里不光可以练习 go 的基本语法和常识,还可以选择其他的语言,大部分的编程语言都是可以学习的,通过一个个任务去完成来掌握一门编程语言,很有成就感。 在掌握了基本的语法后就需要项目来练手,但是不知道应该写些什么东西的时候可以参考gophercises这个网站,里面会有各种练手项目,比如如何生成一个短链接,如何制作一个文件重命名工具等等。 经过两个网站的学习,再加上一点点悟性,相信很快就可以上手了

January 22, 2022 · 1 min · Max

用 go 实现一个简易聊天室

概述 first,我们需要创建一个房间,可以使用 net 包直接启动一个常驻的 tcp 服务 second,我们需要有用户的信息,用户通过访问 tcp 服务来进入房间 end,我们需要在用户进入/离开房间时对其他用户进行广播,还需要将用户在房间内产生的消息发送到房间里让其他用户看见 实现 我们通过设定三个 channel 来分别标记用户进入房间、用户离开房间和用户广播消息的存储。 首先,我们创建一个 tcp 服务器,新建一个 server.go 的文件,然后通过 net 包来初始化 tcp 服务器,端口就选择 2022。监听器创建完毕后我们可以看到它里面有三个方法,分别是 Accept、Close 和 Addr,Close 就是用来关闭这个端口监听,Addr 就是返回监听器所对应的 ip,这里默认的话也就是本地的 ip,Accept 方法就是返回一个连接,一旦有人请求这个文件,就意味着产生一个连接,如果没有人请求那么 Accept 就会等待,这里我们用一个死循环来让它可以重复的接受多个连接请求。 listener, err := net.Listen("tcp", ":2022") if err != nil { panic(err) } for { conn, err := listener.Accept() if err != nil { log.Println(err) continue } go handleConn(conn) } 然后就是 handleConn 这个方法,将每次的连接传入进去,然后初始化连接的用户信息,再通过一个协程将用户消息通道里的信息拿出来消费,接下来做的就是向用户进入/离开的消息 channel 中写入该用户已经上线或用户离开的消息,然后再将用户记录到用户列表中。最后通过扫描该连接来读取用户的输入内容。 func handleConn(conn net.Conn) { defer conn.Close() // 1. 新用户进来,构建该用户的实例 user := &User{ ID: GenUserID(), Addr: conn.RemoteAddr().String(), EnterAt: time.Now(), MessageChannel: make(chan string, 8), } // 2. 由于当前是在一个新的 goroutine 中进行读操作的, 所以需要开一个 goroutine 用户写操作。读写 goroutine 之间可以通过 channel 进行通信 go sendMessage(conn, user.MessageChannel) // 3. 给当前用户发送欢迎信息,向所有用户告知新用户到来 user.MessageChannel <- "Welcome, " + user.String() messageChannel <- "user:`" + strconv.Itoa(user.ID) + "` has enter" // 4. 记录到全局用户列表中,避免用锁 enteringChannel <- user // 5. 循环读取用户输入 input := bufio.NewScanner(conn) for input.Scan() { messageChannel <- strconv.Itoa(user.ID) + ":" + input.Text() } if err := input.Err(); err != nil { log.Println("读取错误:", err) } // 6. 用户离开 leavingChannel <- user messageChannel <- "user:`" + strconv.Itoa(user.ID) + "` has left" } 上面这段代码有严格的执行顺序,我们可以看到最后连用户离开消息也一并发送但是却没有看到判断连接断开的代码,那是因为上面监听用户输入的代码阻塞了流程,当用户断开时自然就会往下走了。如果要手动判断 net.Conn 是否断开,然后再继续操作的话,可以使用一个 byte 类型的切片,将当前时间通过 net 包下的 SetReadDeadline 函数传入,然后使用 net 包下的 read 方法来读取这个 byte 切片,当发生错误信息并且错误信息是 io.EOF 类型时,说明连接已经断开,具体代码可以参考这里:Best way to reliably detect that a TCP connection is closed ...

January 22, 2022 · 2 min · max

将视频切割为均等的时间长度

前情提要 有时候在某些软件中将单次可发送的视频时长限制为 5 分钟或者 15 分钟, 总之就是一个固定的长度, 这样在我们发送超过时长的视频时会比较不方便. 所以写一段程序来自动按照传入的时间来等分视频. 当然这只是其中一个场景. 利用 goroutine 和 FFmpeg 首先获取视频长度信息可以通过 ffprobe 命令来获取, 这个是 FFmpeg 下的一个工具包, 安装 FFmpeg 后就会有了. 然后拿到视频总时长后根据传入的分钟数来分割视频, 这时候就需要用到 ffmpeg 命令了. 最后启动 goroutine 来异步分割每一段视频, 最终完成. 工具地址 https://github.com/muyehub/cut_video_avg

December 2, 2021 · 1 min · max

golang 编程陷阱---摘自《go 专家编程》

关于切片扩容 在使用 append 向 slice 中追加元素时,如果 slice 容量不足以存储新元素,则会把当前切片扩容并产生一个新的切片。如下: package main import "fmt" func main() { s := make([]int, 0) w := append(s, 1) t := append(s, 2) v := append(s, 3) fmt.Printf("%p, %p , %p , %p, %p", s, w, t, v) } // 0x116ce80, 0xc00001c198 , 0xc00001c1a0 , 0xc00001c1a8 append 会每次都生成一个新的切片,并且每次都会做扩容操作,元切片 s 并没有改变任何。 所以,在操作中要将 append 的返回值接收,如果不接收编译器会报错,如果用 _ 忽略返回值,则需要考虑扩容的情况,避免滥用。 空切片 向 slice 中 append 空值(nil)时也会增加 slice 的长度,这在有些情况下可能会导致严重的错误,并且不易察觉,书中举了一个 Kubernetes 项目中的例子。 有一个错误收集器,用来将验证函数的一系列错误收集到一个 slice 中,检查 slice 的长度如果大于 0 时,说明有错误产生,则程序退出,如下: package main import ( "errors" "os") func main() { var errs []error errs = append(errs, ValidateName("张三")) if len(errs) > 0 { println(errs) os.Exit(1) } } func ValidateName(name string) error { if name != "" { return nil } return errors.New("empty name") } 在使用 append 函数时,谨记 append 可能会追加 nil 值,应该尽量避免追加无意义的元素。 ...

November 2, 2021 · 2 min · max

php-tips

在 foreach 中不要使用 array_merge,不仅慢而且耗费内存,可以在 foreach 中对要合并的数组进行赋值,在 foreach 外部统一进行合并,具体性能差异可以参考这篇文章:不要在循环体中使用 array_merge() 在 foreach 中也不要使用 array_push,可以通过向数组中赋值的方式代替,比如:$students[] = $student[‘id’], 而不是 array_push($students, $student[‘id’]),具体性能差异可以参考这篇文章:不要在循环体中使用 array_push() 针对一些查询较为复杂或者比较核心的逻辑里的 sql 查询,可以将 sql 语句写到注释中,这样看代码的时候不用再去拼接 sql,直接用注释中的 sql 拿来排查问题 如果使用 phpstorm 可以安装 php inspections 插件来进行代码的静态分析,可以帮助查找代码规范,语法漏洞,函数使用方式方面的错误。其他的一些代码质量工具可以配置使用 phpcs,phpmd,phpcs-fixer,psalm,phpstan 等。而且可以通过 phpstorm 的热键录制功能,将徽标键 + s 设置为 phpcs-fixer,format code 的快捷键,这样写完时可以通过快捷键格式化代码。 为避免在项目中将 dd,var_dump,exit,die 等函数提交到远程版本库中,可以在项目的 .git/hooks/pre-commit 中进行控制,加入下面的代码,每次提交时可以检测到。参考的这个:pre-commit VAR=$(git diff --cached --diff-filter=ACMR | grep -wiE "var_dump|echo|exit|dd|die|console.log") if [ ! -z "\$VAR" ]; then echo "You've left a dd, var_dump, exit, die or console.log in one of your files! You don't really want to commit this so aborting now..." exit 1 fi 在 for 循环的条件中不要使用函数,例如使用 count 来计算总数,这样每次循环都会执行一遍,不太好 ...

August 16, 2021 · 1 min · max

go-tips

当把时间作为一个变量传递时,可能会导致的问题,比如: waitTime := 30 wait := waitTime * time.Second 这样子是错误的,因为 waitTime 不是 time.Duration 类型,但如果直接写 30 * time.Second 就会成功 在不太了解的情况下,我手动把 waitTime 通过“诡异”的方式变成了 time.Duration 类型 waitTime := 30 * time.Second wait := waitTime * time.Second 这种情况下 wait 的值就变成了 88 万小时,不是预想的结果,正确的写法如下: waitTime := 30 wait := time.Duration(waitTime) * time.Second 当 waitGroup 使用时要清楚的知道 Done 方法的执行次数,如果是类似递归的结构,执行了多次 Done 方法,则会导致waitGroup 的计数器溢出,导致边界错误,如下: 这段代码运行时会导致 panic: sync: negative WaitGroup counter,所以我们在实际编写代码的过程中,如果有类似的情况,可以通过将有限次数的递归转换为迭代的形式来规避这种问题。总之,你需要明确的知道 Done 方法执行了几次。 在 goroutine 使用的过程中我们是无法拿到当前 goroutine 的报错信息的,从而可能需要额外建立一个通道,将报错信息传递到某一个 channel 中,然后监听 channel 来进行错误处理。官方提供了一项实验功能,可以用来解决这个问题。官方包在这里:golang.org/x/sync/errgroup 在开发过程中可以使用 go vet 工具来检测可能会出现的代码漏洞,帮助查漏补缺。使用方式 在开发时要使用 internal 下的 log 包打日志才能被 ELK 捕获到,logrus 则不行,但如果命令行中的即使反馈日志,或者即时性比较强没有留存意义的日志,理当不计入 ELK 的统计当中,这时候需要用 logrus 字符串拼接使用哪种方式更快呢? 已知的四种方式分别是:1. 使用 fmt.Sprintf 函数 2. 使用加号拼接 3. 使用 strings 包 4. 使用 bytes ...

August 16, 2021 · 1 min · max