Hi there 👋

Take my broken heart, make it into art.

由 append 引发的一个疑问  [draft]

由 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....

February 20, 2022 · 3 min · Max

golang 中提防竞态条件的产生  [draft]

我们来看下面这段代码: 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 进行原子操作,不管结果是否正常。因为如果不这么做,有天出了问题可是不好排查的。...

February 9, 2022 · 1 min · Max

控制 goroutine 执行顺序的一例  [draft]

依次打印 题目:依次输出 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....

February 8, 2022 · 2 min · Max

推荐两个学习 go 的网站  [draft]

一个就是 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....

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 时,说明有错误产生,则程序退出,如下:...

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 [ !...

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....

August 16, 2021 · 1 min · max

一道go面试题

一道 Go 面试题 请问下面这两段代码分别输出什么? 直接说结果, 如果你的电脑不是奔腾, 赛扬什么的处理器, 那结果基本是固定的, 就是啥也没有. 因为在 goroutine 反应过来之前, 程序已经执行完了, 还没来得及输出就结束了. 要么, 你就让时间延迟几秒再结束, time.Sleep(10 * time.Second), 要么就像下面这样使用 WaitGroup 来等待 goroutine 结束 这不是问题的重点, 重点是这两种使用匿名函数的方式, 输出会有什么不一样吗? 首先说第一种, 没有传参的情况下, 匿名函数中是使用的引用地址来操作的, 这时候 i 可能已经加到 10 了, 所以打印出来可能会是 10 个 10, 或者中间夹杂几个其他数字什么的. (而且这时候 goroutine 总是在 i++ 之后执行, 所以是从 1 - 10, 不是 0 - 9) 第二种情况, 显而易见就是乱序的了, 会把 0 - 9 挨个输出, 但是是乱序的

January 18, 2021 · 1 min · max