Hi there 👋

Take my broken heart, make it into art.

用 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

一道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

i++是原子操作吗?

i++ 是原子操作吗? 不是, i++ 要完成的操作要先读取变量 i 的值, 对这个值加 1, 再把结果保存到 i 中. 所以不是原子操作. 我们来看下面的代码 这段代码是启动 10 个线程, 每个线程对 count 执行 10 万次自增操作, 直观上来看最后 count 的结果是 100 万, 但因为自增操作不是原子性的, 所以最终的结果每次都会不一样, 会有协程做重复操作. 那应该怎么解决这个问题呢? 并发资源的读取问题最直接的解决方法就是加锁. 改良后如下:

January 17, 2021 · 1 min · max

Gerrit概念说明及使用

Gerrit介绍 Gerrit简介 Gerrit, 一种开放源代码的代码审查软件, 使用网页界面. 利用网页浏览器, 同一个团队的软件开发者, 可以相互审阅彼此修改后的代码, 决定是否能够提交, 回退或是继续修改. 它使用版本控制系统Git作为底层. 它分支自Rietveld, 作者为Google公司的Shawn Pearce, 原先是为了管理Android项目而产生. 这个软件的名称, 来自于荷兰家具设计师赫里特·里特费尔德(Gerrit Rietveld). 因为对访问控制表(ACL)相关的修正, 没有被集成进Rietveld, 之后Gerrit就由Rietveld分支出来, 形成独立软件项目. 最早它是由Python写成, 在第二版后, 改成用Java与SQL. 使用Google Web Toolkit来产生前端的JavaScript. 为什么需要Gerrit 首先, 代码审查可以帮助程序员了解系统功能, 从整体掌控代码质量, 其次, 通过代码审核可以及时止损, 构建更加健壮的系统代码. 代码审核的建议(来自程序员客栈) 对事不对人, 大家都是同事, 在一个团队工作和气最重要. 不要在Code Review中说"你写的什么垃圾"这种话, 你可以说"这个变量名不是很好理解, 咱们换成xxx是不是更好" 每个Review至少给一条正面评价. Gerrit中有对代码点赞的功能, 可以时不时的使用一下. 保证发布的代码和评审意见的可读性. 用工具进行基础问题的自动化检查. 用Tab还是空格, 用两个空格还是四个空格, 缩进风格是使用K&R还是Allman. 这些问题可以使用php code sniffer解决, 团队应该把精力放在代码规范, 代码性能优化等地方. 全员参加Code Review, 并设定各部分负责人. 每个代码PR(Pull Request)内容一定要少. Code Review效果和质量与PR代码量成反比, 提交的代码越多, Code Review的效果就越差. 所以要经常Code Review, 保证每个PR代码的量要少, 最多不超过300行/PR. ...

September 23, 2020 · 2 min · max

为什么没有使用索引?

为什么没有用索引? 已知表 a 数据总量是 40w, 根据时间范围过滤出最近 7 天的数据, 语句如下: explain SELECT `question_id`, `student_id`, `teacher_id`, `question_type`, `question_step`, `question_no`, `level`, `class_id`, `lesson_id`, `teacher_get_at`, `teacher_set_at`, `status`, `teacher_from`, `wrong_reason_id`, `created_at`, `submit_time` FROM `answer_records` WHERE `updated_at` BETWEEN '2020-07-14+00:00:00' AND '2020-07-21+00:00:00' ORDER BY `created_at` DESC; 从 explain 返回的数据来看, 这条语句并没有使用到索引, 而且 mysql 进行了全表的扫描, 这个原因就是我们要查询的数据太多了. 因为 innodb 引擎会在检索索引后进行回表的操作, mysql 觉得你查询的数据这么多, 我一个个回表的这点时间都可以把全表扫一次了, 我就没必要用索引再去回表了, 所以就会导致这个索引没有用到. 那我们就想用索引, 应该怎么办呢? FROM `answer_records` FORCE INDEX(rqx) 在业务层面的话, 如果时间范围比较大, 可以分批次查询, 这样就会快一点, 如果是频繁需要此类操作的话, 还是建议将时间戳提早设计进表结构里, 通过 int 类型的时间戳进行范围查找和排序会事半功倍

September 23, 2020 · 1 min · max

在图片上添加文字或图片

intervention/image 这是一个第三方扩展, 用来处理图片的, 功能特别强大, 可以 cover 我们这篇文章提到的需求 安装文档: http://image.intervention.io/getting_started/installation 我们首先在项目中引入这个包: $ composer require intervention/image 其次, laravel 中需要发布配置文件: php artisan vendor:publish --provider="Intervention\Image\ImageServiceProviderLaravel5" lumen 中需要修改 bootstrap.php, 在当中手动注册 $app->register(\Intervention\Image\ImageServiceProviderLumen::class); 该扩展默认使用的是 GD 库, 如果你想用 Imagick 扩展需要修改文件, config/image.php <?php return [ 'driver' => 'imagick' ]; 如果你不想要每次 use 都使用全路径, 可以按照官方所述将 Image 关键字写入 alias 中 Laravel: config/app.php $providers: Intervention\Image\ImageServiceProvider::class $aliases: 'Image' => Intervention\Image\Facades\Image::class Lumen: bootstrap/app.php $app->withAliases(array('Intervention\Image\Facades\Image' => 'Image')); 具体使用 use Image; public function deal() { $image = Image::make(public_path() . '/foo.jpg'); $text = '我是天才'; $image->text($text, 620, 200, function($font) { $font->file(public_path() . '/SimHei.ttf'); $font->size(200); $font->color('#fdf6e3'); $font->align('center'); $font->valign('top'); $font->angle(0); }); return $image->response('jpg'); } 插入文字的方法是 text, 插入图片的方法是 insert, 如果文字插入后显示乱码, 考虑换一种字体, 支持中文的, 默认的好像就是乱码 ...

January 10, 2020 · 1 min · max