Go: 判断空字符串的性能比较

字符串在 Go 内部中是一个结构体,一个字段记录长度,一个字段记录指针位置。

判断字符串为空最常见的是 data == "" 这样,之前我认为这里分配了一个空字符串会占用分配的时间,所以我把一些判断都改成了 len(data) == 0 这样就不用分配一个空字符串的空间了,效率应该会提升。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"unsafe"
)

func main() {
data := ""
fmt.Println(unsafe.Sizeof(data)) // 输出 16
}

但最近我在 Go 源码中看到很多使用第一种判断是否为空的写法,这让我有点怀疑是否我的优化是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package comp

func isEmptyString0() bool {
var data string
if data == "" {
return true
}
return false
}

func isEmptyString1() bool {
var data string
if len(data) == 0 {
return true
}
return false
}
````

测试后,发现二者在性能上几乎没有区别。

```go
package comp

import "testing"

func BenchmarkIsEmptyString0(b *testing.B) {
for i := 0; i < b.N; i++ {
isEmptyString0()
}
}

func BenchmarkIsEmptyString1(b *testing.B) {
for i := 0; i < b.N; i++ {
isEmptyString1()
}
}

测试输出:

1
2
3
4
5
6
7
8
goos: windows
goarch: amd64
pkg: github.com/islishude/test/str
BenchmarkIsEmptyString0-8 2000000000 0.30 ns/op 0 B/op 0 allocs/op
BenchmarkIsEmptyString1-8 2000000000 0.30 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/islishude/test/comp1.541s
Success: Benchmarks passed.

结果一致,另外看到内存也都没有,这说明 go 在编译时已经对这个做了优化。

await 技巧

不要在同步函数使用 await

async/await 可以接受同步函数,相当于 await Promise.resolve ,今天在个异步流程中发现一个同步函数使用了 await ,结果上没什么区别,但是会有性能损失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function t1() {
const data = [1, 2, 3];
const res = await data.map(val => val ** 2);
return res;
}

async function t2() {
const data = [1, 2, 3];
const res = data.map(val => val ** 2);
return res;
}

console.time(1);
t1().then(res => console.log(res));
console.timeEnd(1);

console.time(2);
t2().then(res => console.log(res));
console.timeEnd(2);

性能对比:

1
2
3
4
1: 0.121ms
2: 0.055ms
[ 1, 4, 9 ]
[ 1, 4, 9 ]

不要使用 return await fn()

最近评审代码发现 return await fn() 这样的写法。其实这样写是多于的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
async function tmp0() {
return await new Promise(res => {
setTimeout(() => {
console.log("tmp0");
res(1);
}, 1000);
});
}

async function tmp1() {
const tmp = await new Promise(res => {
setTimeout(() => {
console.log("tmp1");
res(1);
}, 1000);
});
return tmp;
}

async function tmp2() {
return new Promise(res => {
setTimeout(() => {
console.log("tmp2");
res(1);
}, 1000);
});
}

async function test() {
const [v0, v1, v2] = await Promise.all([tmp0(), tmp1(), tmp2()]);
console.log(v0, v1, v2);
}

test()
.then(console.log)
.catch(console.log);

输出

1
2
3
4
tmp0
tmp1
tmp2
1 1 1

可以看到这里都能返回 1

如果这样使用在 ESLint 中会报错,no-retrun-await. 因为这是多于的。

捕获 await 错误的一个巧妙方式

在 async/await 函数中通常使用 try { await …} catch {} 的形式捕获 async 内部错误。

不过如果这个 await 后的函数不需要中断 async 函数,可以直接在 await 后使用 catch 即可。

1
2
3
4
5
6
7
8
async function test() {
await Promise.reject(0).catch(() => console.log(1));
return 4;
}

test()
.then(i => console.log("test.then => %s", i))
.catch(e => console.log("test.catch => %s", e));

输出:

1
2
1
test.then => 4

注意,这里仅仅是说明这个 await 函数并不需要中断整体服务的情况下才可以。

这样的写法更为简洁。

Go: 声明即初始化

因为分离编译的原因,在 C++ 中声明和定义是不同的概念。在其它语言中,定义和声明几乎没有区别。

1
2
3
extern int i; // 声明
int j = 0; // 定义
int h; // 定义并初始化

C++ 默认初始化的规则是在函数外定义的变量拥有初始值,而在函数内部没有。c++ 就是这么多规矩。如果不注意的话就会运行时错误。

在 JavaScript 因为动态类型的原因并不能默认初始化,所有变量未赋值则为 undefined。

1
2
let i; // undefined
let j = 1;

如果使用 TypeScript 的话,需要开启 strict: true,如果不开启的不会直接进行 null check,就算是 undefined 也可以在代码中使用 。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IPerson {
name: string;
age: number;
}

let student: IPerson = void 0;

function getName(std: IPerson) {
return std.name;
}

// 这里会报运行时错误
getName(student);

而在 Go 中声明即初始化,如果没有初始化值,那么就会赋值为零值

1
2
3
4
5
6
7
8
9
10
11
int     0
int8 0
int32 0
int64 0
uint 0x0
rune 0 // rune的实际类型是 int32
byte 0x0 // byte的实际类型是 uint8
float32 0 // 长度为 4 byte
float64 0 // 长度为 8 byte
bool false
string ""

而对于指针、函数、接口、切片、通道、字典(映射)的零值都是 nil。

有了零值在就很大程度避免了运行时错误。

1
2
3
4
5
6
7
8
9
10
11
12
package main

type person struct {
name string
age uint8
}

var p person

func main() {
fmt.Println(p.name, p.age)
}

另外这种初始化是递归进行的。如果未指定任何值,则结构数组的每个元素都将其字段归零。

1
2
3
4
5
type T struct {
n int
left *T
}
t := new(T) // t.n == 0, t.left == nil

通常 golang 初学者会犯 nil map 的错误,下面会报 panic: assignment to entry in nil map 错误。这里的 m 是 nil ,最终也会报运行时错误。

1
2
3
4
5
func main() {
var m map[string]string
m["name"] = "test"
fmt.Println(m["name"])
}

为什么声明了而没有赋初始值呢?因为 map 内部是一个引用类型的结构,如果 map 初始化需要使用 make 。

记住:最佳实践对于 slice map 和 channel 需要使用 make

这对于指针也同样适用,如果定义了指针而没有初始化指针值,那么也会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

package main

import (
"encoding/json"
"fmt"
)

func main() {
// 使用上面定义 person 结构
var p *person

var data = `{"name":"test","age":20}`
err := json.Unmarshal([]byte(data), p)

if err != nil {
fmt.Println(err)
}
}

那么这里就会打印错误:json: Unmarshal(nil *main.person)

避免这样的问题,就需要一个非 nil 指针,很简单,使用 new 函数即可。

1
2
3
4
5
6
7
8
9
10
func main() {
p := new(person)

var data = `{"name":"test","age":20}`
err := json.Unmarshal([]byte(data), p)

if err != nil {
fmt.Println(err)
}
}

所以所有语言的最佳实践都是要始终初始化变量。

现在还有一个 gopher 可能遇到的问题,上面说 interface 接口会初始化为 nil。

比如说下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"io"
)

func main() {
var tmp io.Writer

if tmp != nil {
fmt.Println("tmp isn't nil")
} else {
fmt.Println("tmp is nil")
}
}

输出的肯定是 tmp is nil。

但是我们改一下。把一个指针为 nil 的赋值为一个接口,那么这个接口也是 nil 吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"bytes"
"fmt"
"io"
)

func main() {
var buf *bytes.Buffer
var tmp io.Writer = buf

if tmp != nil {
fmt.Println("tmp isn't nil")
} else {
fmt.Println("tmp is nil")
}
}

答案是输出 tmp isn't nil

因为对于接口 interface 内部存储而言,一个字段存储类型一个字段存储内容。内容为 nil 的接口,本身并不是 nil。

go generate 预处理器教程

通常编译 protobuf 会使用 protoc 手动编译,更好一点可以写一个 Makefile 指令来做。

不过在 Go 中提供一种在源文件定义类似 Makefile 指令 generate。当运行 go generate 后,编译器会找到所有包含 //go:generate command argument... 的注释,然后运行后面的命令。

那这样的话就不需要再写一个 Makefile 指令了。

使用 go generate 工具编译 protobuf

新建一个文件目录 test,然后编辑 doc.go 文件。BTW,doc.go 是约定俗成写包文档的文件,通常不会写逻辑代码,所以这里写 generate 指令最好不过了。

1
2
3
//go:generate protoc --go_out=. *.proto

package test

generate 指令只能在 go 文件中使用,而且需要注意的是和传统注释不同的是 // 后面不能有空格。

然后编辑 test.proto 文件

1
2
3
4
5
6
7
syntax="proto3";

message Info {
uint32 info_id = 1;
string title = 2;
string content = 3;
}

另外 go build 等其它命令不会调用 go generate,必须手动显式调用 go generate 。不过这报错了,提示找不到文件。

1
2
*.proto: No such file or directory
doc.go:1: running "protoc": exit status 1

这个问题在文档里有说明,generate 并不处理 glob。那我们这里修改 doc.go 当 sh 直接处理就行了。

1
2
3
//go:generate sh -c "protoc --go_out=. *.proto"

package test

另外也要注意,双引号会被 go 进行解析,所以该转义的地方需要注意转义。

自动生成 Stringer 接口

在 golang 博客中 generate code介绍了一种类似宏指令的方式。

假设定义一组不同类型的药物的枚举:

1
2
3
4
5
6
7
8
9
10
11
package painkiller

type Pill int

const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)

通常为了能直接 print 其枚举名称,我们会给 Pill 实现 Stringer 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
func (p Pill) String() string {
switch p {
case Placebo:
return "Placebo"
case Aspirin:
return "Aspirin"
case Ibuprofen:
return "Ibuprofen"
case Paracetamol: // == Acetaminophen
return "Paracetamol"
}
return fmt.Sprintf("Pill(%d)", p)
}

不过有了 generate 指令我们可以不用手写这些逻辑代码。

下载并安装 stringer

1
go get golang.org/x/tools/cmd/stringer

在 Pill 包名称上添加一句 //go:generate stringer -type=Pill。通常为了和文档区分开,我们还要加一个空行。

1
2
3
4
5
6
7
8
9
10
11
12
13
//go:generate stringer -type=Pill

package painkiller

type Pill int

const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)

这时候会自动生成 pill_string.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// generated by stringer -type Pill pill.go; DO NOT EDIT

package painkiller

import "fmt"

const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"

var _Pill_index = [...]uint8{0, 7, 14, 23, 34}

func (i Pill) String() string {
if i < 0 || i+1 >= Pill(len(_Pill_index)) {
return fmt.Sprintf("Pill(%d)", i)
}
return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}

命令格式

1
2
3
4
5
6
7
go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]

-v 输出被处理的包名和源文件名
-n 显示不执行命令
-x 显示并执行命令

-run 正则匹配要运行的指令

还可以在命令中定义别名,不过只有当前文件内有效。

1
2
//go:generate -command YACC go tool yacc
//go:generate YACC -o test.go -p parse test.y

另外还支持下面这些变量:

1
2
3
4
5
6
7
8
9
10
11
12
$GOARCH
CPU架构 (arm、amd64等)
$GOOS
操作系统类型(linux、windows等)
$GOFILE
当前处理中的文件名(就是文件名,不包含路径)
$GOLINE
当前命令在文件中的行号
$GOPACKAGE
当前处理文件的包名(只是当前包名,不包含路径)
$DOLLAR
美元符号

redis 安装清单

redis 推荐使用在 linux 下安装。

下载编译

打开 https://redis.io/download 选择稳定版本下载,按照下列方式进行编译安装。

1
2
3
4
5
6
7
8
mkdir down
cd down
wget http://download.redis.io/releases/redis-4.0.8.tar.gz
tar -zxvf redis-4.0.8
cd redis-4.0.8
make
sudo make install
which redis-cli

最后使用 which 命令就可以看到 redis-cli 被安装到 /usr/local/bin/redis-cli

使用配置文件启动

创建一个 redis/6379-redis.conf

1
2
3
4
5
6
7
8
9
bind 127.0.0.1
port 6379
# 后台启动
daemonize yes
supervised no
logfile .
# 工作目录
# 即持久化目录
dir .

然后就可以使用配置文件启动

1
2
3
redis-server 6379-redis.conf
redis-cli
ping

配置文件模板在下载 redis tar 包中有,即 redis.conf 可以复制下来

1
cat redis.conf | grep -v "#" | grep -v "^$" | tee 6379-redis.conf

上述命令可以将配置文件模板中中的注释和空行去掉并重定向配置到一个新文件。

Node.js 常见错误解决方式

Unexpected end of JSON input while parsing near

npm install 出现 ”Unexpected end of JSON input while parsing near” 的错误。

运行 npm cache clean –force

国内网络慢?node-sass安装错误

使用cnpm进行安装,可以解决常见的 node-sass 遇到错误问题,另外还支持 gzip 压缩。当下载安装 Nodejs+npm 完成后可以使用以下命令安装。 #89 推荐使用yarn包管理工具替代npm

1
npm install -g cnpm --registry=https://registry.npm.taobao.org

凡是包缓存在国外亚马逊云上的都会 出现网络问题,所以开发推荐完成使用 cnpm,如果旧项目中存在 node_moudles 以及 package-lock.json 需要先删除后再使用 cnpm install

或者配置 npm 选择镜像

1
2
3
4
5
npm config set registry https://registry.npm.taobao.org
npm config set disturl https://npm.taobao.org/dist
npm config set electron_mirror https://npm.taobao.org/mirrors/electron/
npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
npm config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/

二进制包编译错误 msbuild.exe failed

如果你使用Windows进行开发,你可能遇到以下错误

1
2
3
4
5
6
7
8
9
10
11
12
gyp ERR! build error
gyp ERR! stack Error: `C:\Program Files (x86)\MSBuild\14.0\bin\msbuild.exe` failed with exit code: 1
gyp ERR! stack at ChildProcess.onExit (C:\Users\chad.lee\AppData\Roaming\npm\node_modules\npm\node_modules\node-gyp\lib\build.js:276:23)
gyp ERR! stack at emitTwo (events.js:106:13)
gyp ERR! stack at ChildProcess.emit (events.js:191:7)
gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:204:12)
gyp ERR! System Windows_NT 10.0.10586
gyp ERR! command "C:\\Program Files\\nodejs\\node.exe" "C:\\Users\\chad.lee\\AppData\\Roaming\\npm\\node_modules\\npm\\node_modules\\node-gyp\\bin\\node-gyp.js" "rebuild"
gyp ERR! cwd C:\dev\archon\webhooks\docs\node_modules\protagonist
gyp ERR! node -v v6.2.2
gyp ERR! node-gyp -v v3.3.1
gyp ERR! not ok

这是因为没有安装 C++ 的编译器造成的,具体方法——

安装 node-gyp

cnpm install -g node-gyp

安装 Visual C++ Build Tools

Windows的安装流程点这里,官方推荐安装的 windows-build-tool的node包进行自动安装配置build-tool,网络下载很慢,我这里不推荐。

配置本地预览服务器ip为0.0.0.0无法打开

Windows 是不支持把 liveloader 的 ip 配制成 0.0.0.0 的,不过有个在不改变配置的同时也能打开预览,那就是把 0.0.0.0 改成 localhost 即可。

有时候在Mac或者Linux上使用 shadowsocks 也会出现这种问题,使用上边的方式也可以解决。至于原因,博主还没有找到。

比特币中交易延展性问题

交易延展性 (Transaction Malleability)

当交易被签名时,签名并没有覆盖交易中所有的数据 (比如位于 txin 中的 scriptSig,由于 scriptSig 中包含公钥和签名数据,不可能对自身自签名),而交易中所有的数据又会被用来生成交易的哈希值来作为该交易的唯一标示。如此,尽管不常见的,比特币网络中的一个节点能够改变你发送的交易 (通过改变 txin 中的签名),导致该交易的哈希值发生变化。注意,攻击者仅仅能够改变该哈希值,交易中的 txout 是无法进行改变的,因此比特币最终也许会转入你原本打算的地址中。然而,这确实意味着,例如,在任何情况下,接收一系列未确认交易的链是不安全的,因为未确认交易的哈希值可能会发生变化,而随后的交易中的 txin 会依赖于先前交易的哈希值。即使交易得到了一个确认,也是不安全的,因为区块链可能会被重新调整。此外,客户端必须一直扫描收到的交易,假定一个 txout 是存在的,因为先前创建该笔交易的客户端可能是不安全的 (可能会发两笔同样 txout 的交易)。

签名延展性 (Signature Malleability)

延展性的第一种形式就是签名本身。每一个签名恰好有一个 DER-encoded ASN.1 octet representation,但是 OpenSSL 并没有强制,只要一笔签名没有极度的改变,它就是可接受的。此外,对于每一个 ECDSA signature(r,s),这个 signature(r,-s(mod N)) 是相同消息的一个有效签名。
在 363742 区块高度处,BIP66 软分叉强制区块链中所有新交易遵循 DER-encoded ASN.1 标准。仍然需要进一步的努力来关闭 DER 签名其它可能的延展性问题。
签名仍然是可以被拥有相应私钥的人改变的。

解锁脚本延展性 (scriptSig Malleability)

比特币中使用的签名算法没有签署 scriptSig 中的任何数据。因为对整个 scriptSig 签名是不可能的,scriptSig 包含签名本身。这意味着,附件的数据能被添加到 scriptSig 中,额外的数据会先于所需的签名和公钥压入栈中。类似的 OP_DROP 能被添加,使得最终栈的状态与 scriptPubKey 执行之前的状态相同。
阻止 scriptSig 延展性正在被考虑当中。目前,如果交易 txin 的 scriptSig 中若包含除了签名与公钥之外其它数据,则该交易被认为是不标准的,不会被节点转发。最终,这个规则会强制在 scriptPubKey 执行完之后,栈中只剩下一个元素。然而,要做到这样,需要比特币随后的扩展。

隔离验证 (segregated witness) 通过将 scriptSig 从交易的 txin 中分离出来,放入到一个新添加的 witness 字段中,来解决相关延展性问题,由于签名数据从交易中分离出来,使得交易结构尺寸变小,可以使得区块能容纳更多的交易,侧面达到了轻微扩容的目的,有关隔离验证的知识,可查看 BIP 141-145。

node.js 中的 process.env 遇到的坑

有这么一段代码,是否会有打印输出 “hello,world” ?

1
2
3
4
5
process.env.dev = false;

if(process.env.dev){
console.log("hello,world");
}

答案是会。

因为 process.env 给任何属性赋值都会先转化为字符串,而字符串在强制转化成布尔值是 true,所以回打印输出。

image

来自官方文档,值得注意的是,以后不可以再给process.env的属性赋值为非字符串类型,否则会再将来的版本中报错。

Assigning a property on process.env will implicitly convert the value to a string. This behavior is deprecated. Future versions of Node.js may throw an error when the value is not a string, number, or boolean.

有空还得多看官方文档啊。

BitcoinCore 安装清单

1
2
3
4
5
6
7
8
sudo apt install software-properties-common
sudo add-apt-repository ppa:bitcoin/bitcoin
sudo apt update
sudo apt install -y bitcoind
# 运行
bitcoind -daemon
# 查看信息
bitcoin-cli getblockchaininfo

ITerm2 技巧

ITerm2 设置 option 键为 Alt 键

image

保持 SSH 连接不自动断开

使用 ITerm2 经常遇到 SSH 服务自动休眠断开,提示下面的信息。

1
packet_write_wait: Connection to x.x.x.x port 22: Broken pipe

修改自动断开可以修改配置配置即可。

image

这里的发送一个 ASCII 码,可以使用 127,也就是发送一个 del 删除键,这样不会影响当前命令行。