LJKのBlog

学无止境

symbol 是一种基本数据类型(primitive data type)。Symbol() 函数会返回 symbol 类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的 symbol 注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:”new Symbol()“。

每个从 Symbol() 返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符;这是该数据类型仅有的目的。更进一步的解析见—— glossary entry for Symbol

1
Symbol([description])

直接使用Symbol()创建新的 symbol 类型,并用一个可选的字符串作为其描述。

  • description:可选

    可选的,字符串类型。对 symbol 的描述,可用于调试但不是访问 symbol 本身。

1
2
3
var sym1 = Symbol();
var sym2 = Symbol('foo');
var sym3 = Symbol('foo');

上面的代码创建了三个新的 symbol 类型。注意,Symbol("foo") 不会强制将字符串“foo”转换成 symbol 类型。它每次都会创建一个新的 symbol 类型:

1
Symbol("foo") === Symbol("foo");  // false

Symbol 值作为属性名时,该属性是公有属性不是私有属性,可以在类的外部访问。但是不会出现在 for...infor...of 的循环中,也不会被 Object.keys()Object.getOwnPropertyNames() 返回。如果要读取到一个对象的 Symbol 属性,可以通过 Object.getOwnPropertySymbols()Reflect.ownKeys() 取到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function a() {
window.fetch("http://101.43.187.22:9501/api/nav/wallPaper").then(async response => {
const resp = await response.json()
console.log(resp)
return resp
})
console.log("a")
}
function b() {
console.log("b")
}

a()
b()

// a
// b
// {code: 200, msg: null, result: Array(8)}

a()b() 是同步执行,a()fetch请求是异步操作。

循环变量只是普通变量,每次循环只是重新赋值。

以下两种写法是等价的:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 写法 1
list := []string{"a", "b", "c"}
var i int
var v string
for i, v = range list {
fmt.Println(i, v)
}

// 写法 2
list := []string{"a", "b", "c"}
for i, v := range list {
fmt.Println(i, v)
}

循环变量 i 和 v 每次循环都会被重新赋值,并非生成新的变量。

这样就产生了一个问题:如果循环体中有并发操作(最常见的是for中起协程),并且并发中用到了 i 和 v,那么 i 和 v 很可能已经不是此次循环的值了,而是下次甚至是最后一次循环的值。

如果要保证协程中用到的循环变量一定就是此次循环的值,就需要 绑定循环变量,两种方式:

  • 协程传参
  • 在协程开始之前,新定义变量,将循环变量赋值给新变量,然后协程中使用新变量

接受者为指针

1
2
3
4
5
func (dog *Dog) SetName(name string) {
dog.name = name
}
(&dog).SetName("大黄") // 调用
dog.Call() // 大黄

该使用值接收者还是指针接收者,取决于是否现需要修改原始结构体:

  • 若不需要修改则使用值,若需要修改则使用指针
  • 若存在指针接收者,则所有方法使用指针接收者

对于接收者为指针类型的方法,需要注意在运行时若接收者为 nil 会发生错误。

1
2
3
4
5
6
7
8
9
10
ffmpeg \
[全局参数] \
{[输入文件参数] -i 输入文件地址} \
{[输入文件参数] -i 输入文件地址} \
{[输入文件参数] -i 输入文件地址} \
... \
{[输出文件参数] 输出文件地址} \
{[输出文件参数] 输出文件地址} \
{[输出文件参数] 输出文件地址} \
...

要注意的是,所有的参数仅仅对仅接下来的文件有效,下一个文件得把参数再写一遍。

所有没有使用 -i 指定的文件都被认为是输出文件。 ffmpeg 可以接受多个输入文件并输出到您指定的位置。

你也可以将输入输出都指定为同一个文件名,不过这个时候要在输出文件前使用用 -y 标记。

你不应该将输入和输出混淆,先指定输入,再指定输出文件。

yarn v1

npm (v5) yarn(1.x) 说明
npm install yarn add 初始化项目
(N/A) yarn add --flat
(N/A) yarn add --har
npm install --no-package-lock yarn add --no-lockfile
(N/A) yarn add --pure-lockfile
npm install [package] --save yarn add [package]
npm install [package] --save-dev yarn add [package] --dev
(N/A) yarn add [package] --peer
npm install [package] --save-optional yarn add [package] --optional
npm install [package] --save-exact yarn add [package] --exact
(N/A) yarn add [package] --tilde
npm install [package] --global yarn global add [package]
npm update --global yarn global upgrade
npm rebuild yarn add --force
npm uninstall [package] yarn remove [package]
npm cache clean yarn cache clean [package]
rm -rf node_modules && npm install yarn upgrade
npm version major yarn version --major
npm version minor yarn version --minor
npm version patch yarn version --patch

yarn v2

https://next.yarnpkg.com/getting-started/install

  1. 全局安装最新版本的yarn v2

    1
    npm install -g yarn@berry
  2. 初始化react项目

    1
    yarn dlx create-react-app my-app --template typescript --use-pnp
  3. 1

初始化项目:

1
yarn init -2

go.mod 示例:

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

go 1.17

require (
github.com/jmoiron/sqlx v1.3.4
go.uber.org/zap v1.21.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.4.0
)

require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
gopkg.in/ini.v1 v1.56.0 // indirect
)

replace (
go.uber.org/zap => go.uber.org/zap v1.21.0
github.com/jmoiron/sqlx => github.com/jmoiron/sqlx v1.3.4
)
  • module:用于定义当前项目的模块路径。
  • go:用于设置预期的 Go 版本。
  • require:用于设置一个特定的模块版本。
  • exclude:用于从使用中排除一个特定的模块版本。
  • replace:用于将一个模块版本替换为另外一个模块版本。

环境变量

GO111MODULE

Go modules 的开关,auto/on/off,推荐 on

1.17 中将完全放弃对 GOPATH 模式的支持,忽略 GO111MODULE 变量设置。

GOPROXY

设置 go 模块代理,用于使 Go 在后续拉取模块版本时能够脱离传统的 VCS 方式从镜像站点快速拉取。它拥有一个默认:https://proxy.golang.org,direct,但很可惜proxy.golang.org在中国无法访问,故而建议使用goproxy.cn作为替代:https://goproxy.cn,direct

“direct” 为特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等),当值列表中上一个 Go module proxy 返回 404 或 410 错误时,Go 自动尝试列表中的下一个,遇见 “direct” 时回源,遇见 EOF 时终止并抛出类似 “invalid version: unknown revision…” 的错误。

GOSUMDB

Go checksum database,用于使 Go 在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经篡改,也可以是“off”即禁止 Go 在后续操作中校验模块版本

默认:sum.golang.org,但是在国内无法访问,故而更加建议将 GOPROXY 设置为goproxy.cn,因为goproxy.cn支持代理sum.golang.org

GONOPROXY / GONOSUMDB / GOPRIVATE

这三个环境变量都是用在当前项目依赖了私有模块,也就是依赖了由 GOPROXY 指定的 Go module proxy 或由 GOSUMDB 指定 Go checksum database 无法访问到的模块时的场景。

其中 GOPRIVATE 较为特殊,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是只是用 GOPRIVATE。

示例:

1
GOPRIVATE=*.corp.example.com

表示所有模块路径以corp.example.com的下一级域名(如:team1.corp.example.com)为前缀的模块版本都将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括corp.example.com本身。

Global Caching

这个主要是针对 Go modules 的全局缓存数据说明,如下:

  • 同一个模块版本的数据只缓存一份,所有其他模块共享使用。
  • 目前所有模块版本数据均缓存在$GOPATH/pkg/mod$GOPATH/pkg/sum下,未来或将移至$GOCACHE/mod$GOCACHE/sum
  • 可以使用go clean -modcache清理所有已缓存的模块版本数据

indirect 表示什么意思

https://my.oschina.net/renhc/blog/3162751

在执行命令 go mod tidy 时,Go module 会自动整理 go.mod 文件,如果有必要会在部分依赖包的后面增加 // indirect 注释。一般而言,被添加注释的包肯定是间接依赖的包,而没有添加 // indirect 注释的包则是直接依赖的包,即明确的出现在某个 import 语句中。

然而,这里需要着重强调的是:并不是所有的间接依赖都会出现在 go.mod 文件中。

间接依赖出现在 go.mod 文件的情况,可能符合下面所列场景的一种或多种:

  • 直接依赖未启用 Go module
  • 直接依赖 go.mod 文件中缺失部分依赖

直接依赖未启用 Go module

如下图所示,Module A 依赖 B,但是 B 还未切换成 Module,也即没有 go.mod 文件,此时,当使用 go mod tidy 命令更新 A 的 go.mod 文件时,B 的两个依赖 B1 和 B2 将会被添加到 A 的 go.mod 文件中(前提是 A 之前没有依赖 B1 和 B2),并且 B1 和 B2 还会被添加 // indirect 的注释。

此时 Module A 的 go.mod 文件中 require 部分将会变成:

1
2
3
4
5
require (
B vx.x.x
B1 vx.x.x // indirect
B2 vx.x.x // indirect
)

依赖 B 及 B 的依赖 B1 和 B2 都会出现在 go.mod 文件中。

直接依赖 go.mod 文件不完整

如上面所述,如果依赖 B 没有 go.mod 文件,则 Module A 将会把 B 的所有依赖记录到 A 的 go.mod 文件中。即便 B 拥有 go.mod,如果 go.mod 文件不完整的话,Module A 依然会记录部分 B 的依赖到 go.mod 文件中。

如下图所示,Module B 虽然提供了 go.mod 文件中,但 go.mod 文件中只添加了依赖 B1,那么此时 A 在引用 B 时,则会在 A 的 go.mod 文件中添加 B2 作为间接依赖,B1 则不会出现在 A 的 go.mod 文件中。

此时 Module A 的 go.mod 文件中 require 部分将会变成:

1
2
3
4
require (
B vx.x.x
B2 vx.x.x // indirect
)

由于 B1 已经包含进 B 的 go.mod 文件中,A 的 go.mod 文件则不必再记录,只会记录缺失的 B2。

总结

为什么要记录间接依赖

在上面的例子中,如果某个依赖 B 没有 go.mod 文件,在 A 的 go.mod 文件中已经记录了依赖 B 及其版本号,为什么还要增加间接依赖呢?

我们知道 Go module 需要精确地记录软件的依赖情况,虽然此处记录了依赖 B 的版本号,但 B 的依赖情况没有记录下来,所以如果 B 的 go.mod 文件缺失了(或没有)这个信息,则需要在 A 的 go.mod 文件中记录下来。此时间接依赖的版本号将会跟据 Go module 的版本选择机制确定一个最优版本。

如何处理间接依赖

综上所述间接依赖出现在 go.mod 中,可以一定程度上说明依赖有瑕疵,要么是其不支持 Go module,要么是其 go.mod 文件不完整。

由于 Go 语言从 v1.11 版本才推出 module 的特性,众多开源软件迁移到 go module 还需要一段时间,在过渡期必然会出现间接依赖,但随着时间的推进,在 go.mod 中出现 // indirect 的机率会越来越低。

出现间接依赖可能意味着你在使用过时的软件,如果有精力的话还是推荐尽快消除间接依赖。可以通过使用依赖的新版本或者替换依赖的方式消除间接依赖。

如何查找间接依赖来源

Go module 提供了 go mod why 命令来解释为什么会依赖某个软件包,若要查看 go.mod 中某个间接依赖是被哪个依赖引入的,可以使用命令 go mod why -m <pkg> 来查看。

另外,命令 go mod why -m all 则可以分析所有依赖的依赖链。

如何锁定版本号

使用 replace

go.mod 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module api.local

go 1.17

require (
go.uber.org/zap v1.21.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.4.0
)

require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
gopkg.in/ini.v1 v1.56.0 // indirect
)
  • module:用于定义当前项目的模块路径。
  • go:用于设置预期的 Go 版本。
  • require:用于设置一个特定的模块版本。
  • exclude:用于从使用中排除一个特定的模块版本。
  • replace:用于将一个模块版本替换为另外一个模块版本。

indirect 表示什么意思

https://my.oschina.net/renhc/blog/3162751

在执行命令 go mod tidy 时,Go module 会自动整理 go.mod 文件,如果有必要会在部分依赖包的后面增加 // indirect 注释。一般而言,被添加注释的包肯定是间接依赖的包,而没有添加 // indirect 注释的包则是直接依赖的包,即明确的出现在某个 import 语句中。

然而,这里需要着重强调的是:并不是所有的间接依赖都会出现在 go.mod 文件中。

间接依赖出现在 go.mod 文件的情况,可能符合下面所列场景的一种或多种:

  • 直接依赖未启用 Go module
  • 直接依赖 go.mod 文件中缺失部分依赖

直接依赖未启用 Go module

如下图所示,Module A 依赖 B,但是 B 还未切换成 Module,也即没有 go.mod 文件,此时,当使用 go mod tidy 命令更新 A 的 go.mod 文件时,B 的两个依赖 B1 和 B2 将会被添加到 A 的 go.mod 文件中(前提是 A 之前没有依赖 B1 和 B2),并且 B1 和 B2 还会被添加 // indirect 的注释。

此时 Module A 的 go.mod 文件中 require 部分将会变成:

1
2
3
4
5
require (
B vx.x.x
B1 vx.x.x // indirect
B2 vx.x.x // indirect
)

依赖 B 及 B 的依赖 B1 和 B2 都会出现在 go.mod 文件中。

直接依赖 go.mod 文件不完整

如上面所述,如果依赖 B 没有 go.mod 文件,则 Module A 将会把 B 的所有依赖记录到 A 的 go.mod 文件中。即便 B 拥有 go.mod,如果 go.mod 文件不完整的话,Module A 依然会记录部分 B 的依赖到 go.mod 文件中。

如下图所示,Module B 虽然提供了 go.mod 文件中,但 go.mod 文件中只添加了依赖 B1,那么此时 A 在引用 B 时,则会在 A 的 go.mod 文件中添加 B2 作为间接依赖,B1 则不会出现在 A 的 go.mod 文件中。

此时 Module A 的 go.mod 文件中 require 部分将会变成:

1
2
3
4
require (
B vx.x.x
B2 vx.x.x // indirect
)

由于 B1 已经包含进 B 的 go.mod 文件中,A 的 go.mod 文件则不必再记录,只会记录缺失的 B2。

总结

为什么要记录间接依赖

在上面的例子中,如果某个依赖 B 没有 go.mod 文件,在 A 的 go.mod 文件中已经记录了依赖 B 及其版本号,为什么还要增加间接依赖呢?

我们知道 Go module 需要精确地记录软件的依赖情况,虽然此处记录了依赖 B 的版本号,但 B 的依赖情况没有记录下来,所以如果 B 的 go.mod 文件缺失了(或没有)这个信息,则需要在 A 的 go.mod 文件中记录下来。此时间接依赖的版本号将会跟据 Go module 的版本选择机制确定一个最优版本。

如何处理间接依赖

综上所述间接依赖出现在 go.mod 中,可以一定程度上说明依赖有瑕疵,要么是其不支持 Go module,要么是其 go.mod 文件不完整。

由于 Go 语言从 v1.11 版本才推出 module 的特性,众多开源软件迁移到 go module 还需要一段时间,在过渡期必然会出现间接依赖,但随着时间的推进,在 go.mod 中出现 // indirect 的机率会越来越低。

出现间接依赖可能意味着你在使用过时的软件,如果有精力的话还是推荐尽快消除间接依赖。可以通过使用依赖的新版本或者替换依赖的方式消除间接依赖。

如何查找间接依赖来源

Go module 提供了 go mod why 命令来解释为什么会依赖某个软件包,若要查看 go.mod 中某个间接依赖是被哪个依赖引入的,可以使用命令 go mod why -m <pkg> 来查看。

另外,命令 go mod why -m all 则可以分析所有依赖的依赖链。

如何锁定版本号

sqlx 完全兼容 sql

增/删/改

Exec

MustExec

Get

Select

事务

开启

Beginx

MustBegin

提交

tx.Commit

回退

tx.

预处理

可以使用 systemd 管理 go 进程

systemd 实践: 依据情况自动重启服务,参考:https://mp.weixin.qq.com/s/W6_Z1zPn3MeDgfGDP263vg

systemd 服务异常自动重启很好用,但有的时候希望某些服务只在特定情况下进行重启,其他时候不要自动重启(比如 OOM,需要人工介入)。 本文抛砖引玉,旨在能够让读者对 systemd 的重启机制有一定了解。

最简单的自动重启范例

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=mytest

[Service]
Type=simple
ExecStart=/root/mytest.sh
Restart=always
RestartSec=5
StartLimitInterval=0

[Install]
WantedBy=multi-user.target

重点参数详解

  • Restart=always: 只要不是通过 systemctl stop 来停止服务,任何情况下都必须要重启服务,默认值为 no
  • RestartSec=5: 重启间隔,比如某次异常后,等待 5(s)再进行启动,默认值 0.1(s)
  • StartLimitInterval: 无限次重启,默认是 10 秒内如果重启超过 5 次则不再重启,设置为 0 表示不限次数重启

案例需求

需求:有个业务,当程序因受到 OOM 而退出的时候,不希望自动重启(此时需要人工介入排查),其他情况下可以自动重启

分析:OOM 就是通过 kill -9 来杀进程,因此只要找到方法,告诉 systemd 当该服务遇到 kill -9 时候不自动重启即可

RestartPreventExitStatus 参数

查询 man systemd.service 发现,systemd 的[Service]段落里支持一个参数,叫做 RestartPreventExitStatus

该参数从字面上看,意思是当符合某些退出状态时不要进行重启。

该参数的值支持 exit code 和信号名 2 种,可写多个,以空格分隔,例如:

1
RestartPreventExitStatus=143 137 SIGTERM SIGKILL

表示,当退出情况只要符合以下 4 种情况中任意一种时候,则不再进行重启

  • exit code 为 143
  • exit code 为 137
  • 信号为 TERM
  • 信号为 KILL

但具体如何使用,请继续往下看

测试方法

/usr/lib/systemd/system/mytest.service

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=mytest

[Service]
Type=simple
ExecStart=/root/mem
Restart=always
RestartSec=5
StartLimitInterval=0
RestartPreventExitStatus=SIGKILL

[Install]
WantedBy=multi-user.target

/root/mem.c(不断消耗内存直至发生 OOM)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main ()
{
char *p = NULL;
int count = 1;
while(1){
p = (char *)malloc(1024*1024*100);
if(!p){
printf("malloc error!n");
return -1;
}
memset(p, 0, 1024*1024*100);
printf("malloc %dM memoryn", 100*count++);
usleep(500000);
}
}

编译及执行

1
2
3
gcc -o /root/mem /root/mem.c
systemctl daemon-reload
systemctl start mytest

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@fzxiaomange ~]# systemctl status mytest
● mytest.service - mytest
Loaded: loaded (/usr/lib/systemd/system/mytest.service; disabled; vendor preset: disabled)
Active: failed (Result: signal) since Sat 2018-10-20 23:32:24 CST; 45s ago
Process: 10555 ExecStart=/root/mem (code=killed, signal=KILL)
Main PID: 10555 (code=killed, signal=KILL)


Oct 20 23:31:55 fzxiaomange.com systemd[1]: Started mytest.
Oct 20 23:31:55 fzxiaomange.com systemd[1]: Starting mytest...
Oct 20 23:32:24 fzxiaomange.com systemd[1]: mytest.service: main process exited, code=killed, status=9/KILL
Oct 20 23:32:24 fzxiaomange.com systemd[1]: Unit mytest.service entered failed state.
Oct 20 23:32:24 fzxiaomange.com systemd[1]: mytest.service failed.

重点看上面第 6 行 MainPID:10555(code=killed,signal=KILL),这行表示主进程的状态,常见有 2 种情况

  • code=exited, status=143:表示 systemd 认为主进程自行退出的,exit code 为 143
  • code=killed, signal=KILL:表示 systemd 认为主进程是被 kill 的,接收到的信号是 SIGKILL

等待 5 秒后,并没有自动重启,符合预期

此时将 RestartPreventExitStatus=SIGKILL 改为 RestartPreventExitStatus=SIGTERM

执行 systemctl restart mytest,再进行一次观察,等待 5 秒后,服务自动重启,符合预期

注意事项

RestartPreventExitStatus 与 Restart 的关系

配置 RestartPreventExitStatus=后,并没有完全忽略 Restart=,而是指当退出情况与 RestartPreventExitStatus=匹配的时候,才忽略 Restart=,若没有匹配,根据 Restart=该怎么样还怎么样(具体详见后面的详细测试数据)

kill 子进程会是什么情况

若 systemd 启动的不是一个简单进程,而是会派生子进程的情况(比如执行 shell 脚本,shell 脚本里启动多个程序),那么当另外开一个窗口通过 kill-信号测试时,会是什么情况呢,先贴出测试方法

ExecStart=/root/mem 改为 ExecStart=/root/mytest.sh

/root/mytest.sh 内容为

1
2
3
4
5
#!/bin/bash

sleep 100000 &

sleep 200000

测试结果

  • 若 kill 主进程 PID(kill 不带参数),则主进程状态为 code=killed,signal=TERM
  • 若 kill -9 主进程 PID,则主进程状态为 code=killed,signal=KILL
  • 若 kill 最后一个子进程 PID(kill 不带参数),则 systemd 不认为是接收到信号,而是根据最后一个进程的 exit code 进行处理,此时主进程状态为 code=exited,status=143
  • 若 kill -9 最后一个子进程 PID,此时主进程状态为 code=exited,status=137

详细测试数据

上面有提到 RestartPreventExitStatus 和 Restart 的关系,但没有数据说明

另外,kill 和 kill -9 的区别,也需要有一份数据说明

因此做了一个详细对比,这里附上详细数据

systemd vs supervisord

https://blog.csdn.net/jackliu16/article/details/79899012