Skip to main content

· One min read

前置依赖

升级 go 1.18

    1. 通过命令行下载
    • go install golang.org/dl/go1.18@latest
    • go1.18 download
    • go1.18 version
    1. 直接下载安装包

支持的代码编辑器

语法

泛型函数

可对比

  • 定义 func A[K comparable](m, n K) K {}
  • 使用 A[string]("abc", "def")

枚举

  • 定义 func B[V int64 | float64](m, n V) V {}
  • 使用 B[int64](123, 456)
func bar[K comparable, V int64 | float64](m map[K]V) V {
// do something...
}

类型推断

  • 输入参数已有明确类型时,支持类型推断
  • 调用函数时,可省略类型声明
ints := []int64{123,456}
B(ints[0], ints[1])

泛型类型

枚举

type Number interface {
int64 | float64
}

在函数泛型参数中使用自定义泛型类型

func B[V Number](m, n V) V {
// ...
}

· One min read
const puppeteer = require('puppeteer');
const pageUrl = 'https://some-url.com';

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterception(true);

page.on('request', (interceptedRequest) => {
// Don't intercept main document request
if (interceptedRequest.url === pageUrl) {
interceptedRequest.continue();
return;
}

// Intercept if request url starts with https
if (interceptedRequest.url.startsWith('https://')) {
interceptedRequest.continue({
// Replace https:// in url with http://
url: interceptedRequest.url.replace('https://', 'http://'),
});
return;
}

// Don't override other requests
interceptedRequest.continue();
})

await page.goto(pageUrl);
await browser.close();
})();

· One min read

今天在 git clone 一个 github 仓库的时候出现了以下错误

ssh_exchange_identification: read: Connection reset by peer

怀疑是代理的原因,试了全局、直连、规则都不行,查找了一些文章有说可以使用以下命令查看详细调试信息

ssh -vvv -T git@github.com

还是不行,在 v2ex 上看到一个楼主说需要加个配置,所以就试了下

~/.ssh/config 中添加以下配置

Host github.com
Hostname ssh.github.com
Port 443

再试下就可以了

· One min read

问题背景

发现生产环境的业务报了好多错误, 涉及的 Node.js 代码是一个基于 Redis 的频率计数器,那部分逻辑大概是这样

// 查询并增加一次计数
async incr (id) {
const key = `${this.namespace}:${id}`
const now = getMicrotime()
const start = now - this.duration * 1000

const operations = [
['zremrangebyscore', key, 0, start],
['zcard', key],
['zadd', key, now, now],
['pexpire', key, this.duration]
]

const res = await this.redis.multi(operations).exec()
const count = toNumber(res[1][1])
return count
}

错误是:

Cannot read property '1' of undefined

· One min read

Mac 环境下 node 安装 canvas@2.6.1 出现以下错误时

node: cairo-pattern.c:1127: cairo_pattern_destroy: Assertion failed. none - catched error

使用 brew 安装一下以下几个库

brew install pixman cairo pango

不过你可能会遇到 python2.x 升级失败的问题

可以试试

brew uninstall python@2
brew install python
brew upgrade python

升级到 python3.x

来源: https://github.com/Automattic/node-canvas/issues/1065#issuecomment-373381272

· One min read

数据库

SELECT default_character_set_name FROM information_schema.SCHEMATA
WHERE schema_name = "schemaname";

SELECT CCSA.character_set_name FROM information_schema.`TABLES` T,
information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA
WHERE CCSA.collation_name = T.table_collation
AND T.table_schema = "schemaname"
AND T.table_name = "tablename";

SELECT character_set_name FROM information_schema.`COLUMNS`
WHERE table_schema = "schemaname"
AND table_name = "tablename"
AND column_name = "columnname";

· One min read
const Fs = require('fs')
const Path = require('path')
const Axios = require('axios')

async function downloadImage () {
const url = 'https://unsplash.com/photos/AaEQmoufHLk/download?force=true'
const path = Path.resolve(__dirname, 'images', 'code.jpg')
const writer = Fs.createWriteStream(path)

const response = await Axios({
url,
method: 'GET',
responseType: 'stream'
})

response.data.pipe(writer)

return new Promise((resolve, reject) => {
writer.on('finish', resolve)
writer.on('error', reject)
})
}

downloadImage()

主要注意的是

  • responseType: 'stream'
  • response.data.pipe(writer)

· One min read

使用另外一个数组来初始化切片时,使用到的下标是一个半开区间的玩意儿,比如

package main

import "fmt"

func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}

var s []int = primes[1:4]
fmt.Println(s)
}

得到的结果是

3,5,7

· One min read

知识一些之前只知道这些名词,但是并不知道这个名词的细节,以下内容仅是个人理解,也可能理解的不到位。

epoll

这个词之前只是尝尝看到对比

  • 同步 IO
  • 异步 IO
  • 阻塞 IO
  • 非阻塞 IO

这几个概念时,会提到的 IO 模型,比如:select、poll、epoll 这里之前描述的有问题,IO 模型中有一种是 IO multiplexing,常译为 IO 多路复用,几种实现是 select、poll、epoll。

而我之前一直不太理解这几个都有啥区别,最近看到知乎的一篇文章,也算大概理解了,可以参考这篇一个EOF引发的探索之路之四(理解golang的NetFD之I/O多路复用篇)

真正从代码层面了解到的是,看到的两个关于优化 golang WebSocket 内存占用的文章,使用 epoll 的方式来接收百万级别的长连接。问题出现的原因是,在通常情况下,我们都会为每个连接分配一个 goroutine ,这在一般情况下是没有什么问题的,而且比其他语言的线程级别的实现要轻量高效的多。但是 goroutine 是没有开销的吗?当然不是!根绝不同的平台,每个 groutine 需要 2K ~ 8K 左右的内存开销(在不做任何事情的情况下,参考 https://github.com/golang/go/blob/release-branch.go1.8/src/runtime/stack.go#L64-L82 ),那么这时候,有什么办法优化么?是有的。

通过 epoll 是如何优化的

通过文章中的代码优化示例(https://github.com/eranyanay/1m-go-websockets/blob/master/4_optimize_gobwas/epoll.go ),我们可以了解到,epoll 方式的是通过接收请求时获取 net.Conn 连接的 fd (File descriptor,具体的值其实是个 int 的值) 文件描述符,然后将文件描述符注册到 epoll 中,每当这个文件描述符所对应的连接有新的数据发送过来时,则会触发我们注册 epoll 时,选择监听的事件,这时,我们再通过这些触发事件的列表信息中的 fd 获取对应的连接,获取到这些连接之后,就可以去获取连接中接收到的数据了。这样只在连接没有任何数据时,并不需要一个固定的 goroutine 的开销。

引申的一些东西

由于涉及到了 net.Conn ,想到了 http 长连接怎么去处理 net.Conn,这时查到了一篇关于请求拦截的文章,参看 https://colobu.com/2016/07/01/the-complete-guide-to-golang-net-http-timeouts/。

还有另外的一些:

zero-copy

意思是零拷贝,这个概念我一开始怎么也猜不到是怎么样实现的,直到看了内存优化的这篇文章 A Million WebSockets and Go 的 3.4 节,发现也不是说完全不用内存,内存还是要的,只是说开辟一块很小的空间,重复利用,但是不对 http 中的原始内容复制到一块新的内存上去再处理,只是每次写入这一块小空间,处理完之后就直接重置这块内存空间,从而达到零拷贝的目的。

fd - File descriptor - 文件描述符

维基百科的解释可参考 https://zh.wikipedia.org/wiki/%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6, 另外一篇讲的还算是具体一点的是这篇 http://c.biancheng.net/view/3066.html

相关链接

· One min read

错误

如果你在使用 nodemailer smtp 发送邮箱时遇到了以下错误:

Error: Invalid login: 535 Error: authentication failed

也许是因为需要使用「客户端授权码」的问题。

解决

const _ = require('lodash')
const nodemailer = require('nodemailer')

async function main () {
const mailer = nodemailer.createTransport({
host: 'smtp.126.com',
port: 465,
pool: true,
secure: true,
auth: {
type: 'login',
user: 'xxxxx@126.com',
pass: 'xxxxx' // 如果开启了客户端授权码,则这里需要填写客户端授权码
},
tls: {
rejectUnauthorized: false
}
})

const sendMailOptions = {
from: 'xxxxxxx@126.com',
to: 'xxxxxx@163.com',
subject: ' 测试主题 ',
html: ' 测试内容 '
}

const result = await mailer.sendMail(sendMailOptions)

if (!_.startsWith(_.get(result, 'response'), '250 Mail OK')) {
return Promise.reject(new Error('Send mail fail'))
}

return result.response
}

main()

参考

https://cnodejs.org/topic/55b78babf30671210b35fa31

· One min read

可以通过这个地址访问到

http://alinode.aliyun.com/dist/new-alinode/alinode.json

其中

{
"version": "v4.7.2",
"date": "2019-03-11",
"files": "linux-x64,osx-x64-tar",
"npm": "6.4.1",
"v8": "6.8.275.32",
"uv": "1.23.2",
"zlib": "1.2.11",
"openssl": "1.1.0j",
"modules": "64",
"node": "v10.15.3",
"notes": [
""
]
}

version 是 alinode 版本,node 是官方 node 版本

· One min read

node-worker-farm 源码解读

node-worker-farm 主要是用来将特定的逻辑,作为子进程的方式执行,由 worker-farm 来统一调度,分发调用任务。

模块构造函数

https://github.com/rvagg/node-worker-farm/blob/master/lib/index.js

可以看到构造函数主要是初始化 farm 容器,将给定的脚本文件路径加载进来等待调用的动作的分发。

可以看到入口有一个 farms 数组,用于存放 farm 容器,当你初始化多个 farm 实例的时候,都会将实例存储在这里,当你需要结束进程时,直接调用 node-worker-farm 暴露的 end 方法,即会销毁所有的 farm 实例。

不过这个特性也许是个坑吧,有人可能会不小心销毁掉不需要销毁的实例。

Farm

Farm 即是整个模块的核心了,它负责的功能有:

  1. 控制子进程的创建/销毁

  2. 新进来的函数执行任务的分发

  3. 控制调用的并发

各个参数的作用:

参数名说明默认值
maxCallsPerWorker每个子进程最多可以处理多少调用Infinity
maxConcurrentWorkers最大并发子进程数CPU 核数
maxConcurrentCallsPerWorker每个子进程最大的并发处理数10
maxConcurrentCalls全局最大并发处理数Infinity
maxCallTime单个 worker 最大处理时间Infinity
maxRetries最大重试次数Infinity
forcedKillTime在进程退出时,如果子进程未正常退出,则会强制退出100ms
autoStart初始化时,就自动最大数量的子进程false
onChild创建子进程时触发的函数空函数

函数调用

每次函数的执行是,构造了一个方法、参数的对象,发送给子进程排队处理。如果有限制最大全局并发数时,达到并发会抛出错误。

https://github.com/rvagg/node-worker-farm/blob/master/lib/farm.js#L312

每次调用函数在内部会执行 addCall 方法,这时就是构造了一个调用的对象信息。将调用信息存入全局队列中,执行 processQueue ,进行分发。

当前活跃的子进程个数小于最大并发子进程数时,创建一个子进程。这时,检查所有子进程正在处理的调用是否小于子进程的并发处理数。小于则将任务分配给这些子进程。

当发送任务时,如果有配置超时时间,则会注册一个超时回调,若超时,则整个子进程会被杀死,如果你的子进程中有多个调用正在执行,那么都会被干掉

处理完之后,通过 receive 方法进行后续的处理。

应用

现在默认的情况下,是最调用效率最优的方案,因为子进程被创建后,有调用的情况下,一直不会被销毁,省去了冷启动的时间。

但是,如果你需要在超时的情况下,cancel 掉正在处理的逻辑,maxCallTime 可以实现这个需求,但有个问题是,子进程被杀死时,所有其他的调用都会被终止,这不符合我们的期望,因此我们需要将 maxCallsPerWorker设为 1 ,让每个子进程同时只处理一个任务,处理完就退出,这样如果任务超时,不会对其他的任务造成影响。

思考

这样做的执行时间将会大幅增加,还有什么办法能够保证任务互不影响的情况,尽可能不降低执行效率呢。

目前想到的思路是,每次虽然只执行一个,但是每次执行完后不将子进程销毁,知道超时的时候才销毁一次,这样的话也可以减少每次创建子进程带来的开销。

你还有更好的方案吗?欢迎在下面留言讨论。

· One min read

如何在 Console 中获取当前 redux 的 store 值

如果你安装了 react developer tools ,你可以通过 $r.store.getState(); 来获取,不用改别的代码.

Note: 你需要先打开 react developer tool 之后,让他可用, 不然你回遇到 $r is not defined error


image.png


  1. 开打 developer tools
  2. 点击 「React」 tab
  3. 确认 provider 节点 (or topmost 节点) 现在是被选中的
  4. 然后输入 $r.store.getState(); or $r.store.dispatch({type:"MY_ACTION"}) 到你的浏览器 console

https://stackoverflow.com/questions/34373462/while-debugging-can-i-have-access-to-the-redux-store-from-the-browser-console

· One min read

印象

这两个名词可能我们曾经在一些文章中见过或者听说过,但是我们可能不知道它到底代表着什么意思。

只知道可能用户态可以做我们代码中编写的一些逻辑的事情,但是具体涉及到什么系统调用之类的操作就需要在内核态才能完成了。

这可能都只是初步印象。

初识

在 《许式伟的架构课》,我学习到,保护模式下,CPU 引入了“保护环 (Protecting Rings)”的概念,用来对 CPU 指令做权限划分。

如 Intel CPU 通常把权限划分为 Ring 0-3 四个等级。

操作系统内核在 Ring 0 (内核态),常规应用软件在 Ring 3 (用户态)。一次中断调用,不只是 “函数调用”,重要的是改变了执行权限,从用户态到了内核态。

因此,我们上面提到的一些情况,应该是由于我们在用户态时,执行权限较低,而某些需要在内核态才能调用的操作只能交给操作系统内核来完成了。

· One min read

较新的 npm 版本可以使用一个新特性来自动给你的 package.json 升级版本号。

为什么需要这个

有很多时候,我们都要去手动改这个版本号,然后再提交一个 commit ,感觉比较麻烦,而且可能有的开发者没有遵循语义化版本 semver 的规则,倒是别人在引用的时候会出现奇奇怪怪的问题。

这个命令怎么用呢

// 升级一个小版本号    1.1.1 -> 1.1.2
npm version patch -m " 升级到 %s ,因为修复了 xxx bug "

// 升级一个中版本号 1.1.1 -> 1.2.0
npm version minor -m " 升级到 %s ,因为添加了 xxxx 特性 "

// 升级一个大版本号 1.1.1 -> 2.0.0
npm version major -m " 升级到 %s ,因为有 xxxx 不兼容的特性 "

会自动修改 package.json 中的版本号,并添加一个 commit 。

· One min read

场景

假如git commit 链是

A -> B -> C -> D

如果想把B,C,D都给revert,除了一个一个revert之外,还可以使用range revert

git revert B^..D 

这样就把B,C,D都给revert了,变成:

A-> B ->C -> D -> D'-> C' -> B'

用法

git revert OLDER_COMMIT^..NEWER_COMMIT

如果我们想把这三个revert不自动生成三个新的commit,而是用一个commit完成,可以这样:

git revert -n OLDER_COMMIT^..NEWER_COMMIT
git commit -m "revert OLDER_COMMIT to NEWER_COMMIT"