0%

某君A 需要在另外一套PostgreSQL数据库中部署一样的数据库表结构。为了偷懒,直接通过图形界面的查询分析器从源数据库中捞出表结构,并在新数据库中创建。数据在新服务器上创建后,发现新服务器无法插入数据,遂找原因,发现是没有找到表索引。查询是查询一张分区表,由于没有在主表上建唯一索引,插入时就找不到相关约束,导致插入失败。在主表上创建唯一索引约束后,插入语句正常工作。

– 2022-02-24 更新

PG11以后的版本,都支持自动创建分区索引功能,只需要在主表上创建相关的索引,所有分区,以及未来创建的新分区表上都会创建相应的索引。

版本号发布

Go模块版本只要分为三个部分如<major>.<minor>.<patch>,例如对于v1.0.1,主版本<major>是1,次版本<minor>是0,补丁版本<patch>是1

v0版本为项目的初始版本,版本不保证稳定行,可以通过git tag来标记版本,将tag推送至仓库即可 v1版本为稳定版本,当你的项目稳定之后,可以标记v1版本对外发布;另外对于稳定之后的预发布版本,由于Go命令首选的是正常版本,而不是预发布版本;可以通过-来制定预发布版本,如v1.0.1-betav1.0.1-alpha;使用的时候需要显示的指定预发布版本go get github.com/p1024k/[email protected] 下面看下怎么发布第一个v0.0.1版本:

创建仓库

首先在你的github仓库创建一个repository,命名为hello,然后将hello项目同步至仓库

标记并推送Tag

通过git标签维护版本。都以master为主仓库,每开发一个版本从master切换出一个分支开发。然后合并回master主分支。在master主分支上执行以下动作。

1
2
git tag v0.0.1
git push origin v0.0.1

在其他项目中引用

在项目中直接导入"github.com/p1024k/hello",然后通过执行 go mod tiny或者go mod build就可以添加新的依赖了

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

import (
"fmt"
"github.com/p1024k/hello"
)

func main() {
fmt.Println(hello.Hello())
}

引入依赖之后 cat go.mod,可以看到已经引入了我们自定义的模块

1
2
3
4
5
6
7
8
module study

go 1.16

require (
github.com/p1024k/hello v0.0.1
)

版本升级

Go模块中规范了一个重要原则

If an old package and a new package have the same import path, the new package must be backwards compatible with the old package.

1
如果旧包和新包具有相同的导入路径,新包必须向后兼容旧包。

v0->v1

假设我们自定义的模块已经稳定了,那么开始要对外发布v1.0.0版本了

  1. 拉取新的分支v1.0.0_branch
  2. git tag v1.0.0 并且推送到远程仓库

v1->v2

v1->v2的升级属于主版本的升级(v2不向后兼容), 这里有两种方式:

  • 创建新的版本目录v2
  • 继续使用v0->v1的升级方式

这里只讲在V1->V2的情况。且只讲继续使用V0->v1的升级方式。

拉取新的分支,修改go.mod路径为github.com/p1024k/hello/v2,基于master分支打tag v2.0.0

1
2
3
4
5
6
7
8
9
10
11
12
13
# checkout from master
git checkout -b v2
go mod edit -module github.com/youdw/hello/v2
...
# make some change
git push
git checkout master
git rebase v2
git tag v2.0.0
git push origin master
git push --tags

# git push origin v2.0.0

创建非默认本地SSH密钥

GitHub不支持相同SSH Key在不同GitHub账户下使用。所以除默认账户外,需要单独为新GitHub账号建立一个SSH Key。注意,使用ssh-keygen生成新key,出现提示输入文件名的时候(Enter file in which to save the key (~/.ssh/id_rsa): id_rsa_new)要输入与默认配置不一样的文件名,比如:我这里填的是 id_rsa_new。否则会覆盖之前生成的默认key。并用ssh-add命令将新创建的Key添加到由ssh-agent 维护的列表中。

ssh-agent 是用于管理SSH private keys的, 长时间持续运行的守护进程(daemon). 唯一目的就是对解密的私钥进行高速缓存。

ssh-add 提示并将用户的使用的私钥添加到由ssh-agent 维护的列表中. 此后, 当使用公钥连接到远程 SSH 或 SCP 主机时,不再提示相关信息.

1
2
ssh-keygen -t rsa -b 2048 -C "[email protected]"
ssh-add ~/.ssh/id_rsa_new

如果出现 Could not open a connection to your authentication agent 的错误,就试着用以下命令:

1
2
$ ssh-agent bash
$ ssh-add ~/.ssh/id_rsa_new
阅读全文 »

PostgreSQL 12: 支持 Generated Columns 特性

PostgreSQL 12 一个给力SQL特性是增加了对 Generated Columns 的支持,这个特性并不陌生,MySQL 已经支持这个特性。

这个特性对分析类场景比较有用,本文简单测试下 PostgreSQL 的这个特性。

发行说明

Add support for generated columns (Peter Eisentraut)

The content of generated columns are computed from expressions (including references to other columns in the same table) rather than being specified by INSERT or UPDATE commands.

补丁说明

1
2
3
4
5
6
7
8
9
10
11
Generated columns

This is an SQL-standard feature that allows creating columns that are
computed from expressions rather than assigned, similar to a view or
materialized view but on a column basis.

This implements one kind of generated column: stored (computed on
write). Another kind, virtual (computed on read), is planned for the
future, and some room is left for it.

Discussion: https://www.postgresql.org/message-id/flat/-4019-bdb1-699e-@2ndquadrant.com

generated column 列的值是根据其它列表达式计算而得,和基于表的物化视图有些类似。

目前仅支持 stored (computed on write), virtual (computed on read) 暂不支持,以后版本会支持。

Generated columns 语法

GENERATED ALWAYS AS ( generation_expr ) STORED

generated column 说明如下:

  • STORED: 表示 Generated columns 类型为 “computed on write”, 数据存储到磁盘上。
  • generation_expr: 只能引用本表的非 generated column 字段,不可以引用其它表的字段,使用的表达式和操作符必须是 immutable 属性。
  • generated column 字段只读: 不支持 INSRET 和 UPDATE。

使用心得:

  • 所谓“使用表达式和操作符必须是immutable属性”就是不能是动态的操作,比如表达式中有函数,它会认为是动态的。
  • 更新相关的字段,generated column 字段中的表达式包含这些字段的话,该generated column字段会被更新。

Generated columns 演示

创建测试表 score 并插入一条测试,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mydb=> CREATE TABLE score(stuid int4, chinese int2, math int2, sum_score int2 GENERATED ALWAYS AS (chinese+math) STORED );
CREATE TABLE

mydb=> \d score
Table "pguser.score"
Column | Type | Collation | Nullable | Default
-----------+----------+-----------+----------+------------------------------------------
stuid | integer | | |
chinese | smallint | | |
math | smallint | | |
sum_score | smallint | | | generated always as (chinese + math) stored


mydb=> INSERT INTO score(stuid,chinese,math) VALUES(1,90,95);
INSERT 0 1

备注: 表 socre 的 sum_score 为 generated column。

验证 sum_score 列的数据,如下:

1
2
3
4
5
mydb=> SELECT * FROM score;
stuid | chinese | math | sum_score
-------+---------+------+-----------
1 | 90 | 95 | 185
(1 row)

Generated columns 不支持 INSERT 和 UPDATE,如下:

1
2
3
4
5
6
7
mydb=> INSERT INTO score(stuid,chinese,math,sum_score) VALUES(1,80,70,100);
psql: ERROR: cannot insert into column "sum"
DETAIL: Column "sum" is a generated column.

mydb=> UPDATE score SET sum_score=100 WHERE stuid=1;
psql: ERROR: column "sum" can only be updated to DEFAULT
DETAIL: Column "sum" is a generated column.

Generated columns 支持创建索引,如下:

1
2
mydb=> CREATE INDEX idx_score_sum ON score USING BTREE(sum);
CREATE INDEX

总结

本文简单演示了 Generated columns 的使用,注意如下:

  • Generated columns 有两种类型, Stored (computed on write) 和 Virtual (computed on read),目前仅支持 stored 类型,数据存储到磁盘上。
  • Generated columns 只能引用本表的非 generated column 字段,不可以引用其它表的字段。
  • Generated columns 使用的表达式和操作符必须是 Immutable 属性。
  • generated columns 字段只读,不支持 INSRET 和 UPDATE。
  • generated columns 支持创建索引。

安装 TCP BBR拥塞算法

https://www.williamlong.info/archives/5586.html 月光博客

TCP BBR(Bottleneck Bandwidth and Round-trip propagation time)是由Google设计,于2016年发布的拥塞算法。以往大部分拥塞算法是基于丢包来作为降低传输速率的信号,而BBR则基于模型主动探测。该算法使用网络最近出站数据分组当时的最大带宽和往返时间来创建网络的显式模型。数据包传输的每个累积或选择性确认用于生成记录在数据包传输过程和确认返回期间的时间内所传送数据量的采样率。

Google在YouTube上应用该算法,将全球平均的YouTube网络吞吐量提高了4%,在一些国家超过了14%。根据实地测试,在部署了最新版内核并开启了 TCP BBR 的机器上,网速甚至可以提升好几个数量级。

  从 4.9 开始,Linux 内核已经用上了该算法,并且对于QUIC可用。如果想在Linux使用BBR,那么首先就是判断内核版本是否大于4.9,如果符合版本标准,那么直接启动BBR就可以了,如果低于4.9,升级内核之后启动就行了。

CentOS 7.3上安装TCP BBR的方法

首先将Centos系统更新,更新到7.3版本。

1
yum update

查看系统版本,输出的release数值大于7.3即可。

1
cat /etc/redhat-release

对于某些机器来说,安装一下wget

1
yum install wget

使用下面命令安装elrepo并升级内核

1
2
3
4
5
6
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel install kernel-ml -y
grub2-set-default 0
grub2-mkconfig -o /etc/grub2.cfg
reboot

更新grub文件并重启(reboot后,ssh会断开,稍等一会儿重新连接)开机后查看内核是否已更换.

1
uname -r

启动BBR。依次执行下面命令就可以了。

1
2
3
echo "net.core.default_qdisc = fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control = bbr" >> /etc/sysctl.conf
sysctl -p

验证bbr是否已经开启

A,验证当前TCP控制算法的命令:

1
sysctl net.ipv4.tcp_available_congestion_control

返回值一般为:

net.ipv4.tcp_available_congestion_control = bbr cubic reno 或者为:

net.ipv4.tcp_available_congestion_control = reno cubic bbr

B,验证BBR是否已经启动。

1
sysctl net.ipv4.tcp_congestion_control

返回值一般为:net.ipv4.tcp_congestion_control = bbr

1
lsmod | grep bbr

返回值有 tcp_bbr 模块即说明 bbr 已启动。

CentOS8上启用TCP BBR方法

1
2
3
4
5
LSB Version:	:core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description: CentOS Linux release 8.1.1911 (Core)
Release: 8.1.1911
Codename: Core

已经自带了BBR

1
2
3
4
5
6
7
8
9
10
11
#加载BBR算法
/sbin/modprobe tcp_bbr
#编辑系统配置文件
sudo vi /etc/sysctl.conf
#结尾添加两行
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
#使修改生效
sysctl -p
#查看已加载算法模块
sysctl net.ipv4.tcp_available_congestion_control

一些程序会输出很多的日志,但是都输出到一个文件中。随着文件尺寸越来越大,会占用很多空间。同时超大文件也不便于操作。需要对日志文件做切割。切割日志有很多方案,这里选用logrotate。

程序启动命令

1
nohup lotus daemon >> logs/lotus.log &

让程序在后台运行,并将日志重定向并追加在日志文件之后。把 > 替换成 >> 追加的形式,就可以被logrotate正常切割。

重定向和追加的区别

> 输出重定向:会将原来的文件内容覆盖

>>追加:不会覆盖原来文件的内容,而是追加到文件的尾部

基本语法

  1. ```
    ls -l > a.txt
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    列表的内容写入文件a.txt(覆盖写)

    - 示例:`ls -l > a.txt`
    - 说明:`ls -l > a.txt`,将`ls -l`的显示内容**覆盖**写入到a.txt文件,如果文件不存在 ,就创建该文件。

    2. `ls -al >> 文件` :列表的内容追加到文件aa.txt的末尾

    3. `cat 文件1 > 文件2` :将文件1的内容覆盖到文件2

    4. `echo "内容" >> 文件`:将内容 追加到文件中

    ## logrotate 文件切割

    在`/etc/logrotate.d/`下建立一个文件 `/etc/logrotate.d/lotus`内容如下:

    /path/to/*.log
    {
    rotate 10
    size 5M
    daily
    copytruncate
    create 664 alpha alpha
    missingok
    notifempty
    compress
    delaycompress
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    **rotate** - 限制日志文件轮转的数量。因此,这将只保留最近的 10 个轮转的日志文件。

    **size** - logrotate 仅在文件大小等于(或大于)此大小时运行。

    **create** - 轮换原始文件并创建具有指定权限、用户和组的新文件。

    **compress** - 压缩文件

    **delaycompress** - 延迟压缩最近一个文件

    **copytruncate** - 🌶️ (有它就能实现文件切割)在创建副本后将原始日志文件截断为零大小,而不是移动旧日志文件和可以选择创建一个新的。它可以在某些程序无法关闭其日志文件时使用,因此可能继续永远写入(追加)上一个日志文件。注意,在复制文件并将其截断,因此某些日志数据可能会丢失。使用此选项时,创建选项将不起作用,因为旧的日志文件仍在原处。

    **daily** - 似乎没有小时级别的日志轮转,未确认。这个选项代表按天执行日志轮转

    想要立即执行,可以执行`logrotate /etc/logrotate.conf`

    ## 查看压缩日志文件

    ```shell
    zcat example.log.1.gz > example.log.1

Here are several alternatives:

  • Give gunzip the --keep option (version 1.6 or later)

    -k --keep

    Keep (don't delete) input files during compression or decompression.
    
    1
    gunzip -k file.gz
  • Pass the file to gunzip as stdin

    1
    gunzip < file.gz > file
  • Use zcat (or, on older systems, gzcat)

    1
    zcat file.gz > file

参考

How To Manage Log Files Using Logrotate In Linux

配置 logrotate 教程

logrotate切割nohup日志大小不变

如何将logrotate与输出重定向一起使用

Unzipping a .gz file without removing the gzipped file

golang提供运行时堆栈调用接口runtime.Caller。所有的日志库都是基于该接口实现日志中打印日志输出所在文件、行号。

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
package main

import (
"fmt"
"os"
"path"
"runtime"
"time"
)

func log(loglvl, format string, args ...interface{}) {
_, file, line, ok := runtime.Caller(0)
if !ok {
panic("asdf")
}

now := time.Now().Format("2006-01-02 15:04:05")

format = fmt.Sprintf("[%s]%s\t%s:%d %s", loglvl, now, path.Base(file), line, format)
fmt.Fprintf(os.Stdout, format, args)
}

func doSomething(data string) {
log("info", "doSomething: %s", data)
}

func main() {
doSomething("hello world")
}

输出结果:

1
[info]2020-04-20 11:42:06	main.go:12 doSomething: [hello world]

这里输出的日志行号显示为12。行号12所在的位置不是实际想要显示的日志位置。实际想要显示的日志位置在24行。这里只需要设置runtime.Caller(1)就可以了。

func Caller(skip int) (pc uintptr, file string, line int, ok bool)

Caller reports file and line number information about function invocations on the calling goroutine’s stack.
The argument skip is the number of stack frames to ascend, with 0 identifying the caller of Caller.
(For historical reasons the meaning of skip differs between Caller and Callers.)
The return values report the program counter, file name, and line number within the file of the corresponding call.
The boolean ok is false if it was not possible to recover the information.

Caller 函数报告有关调用goroutine堆栈上的函数调用的文件和行号信息。
参数skip是要提升的堆栈帧数,其中0标识Caller的调用方。

(由于历史原因,在Caller和Callers之间,跳过的含义有所不同。)
返回值报告相应调用文件中的程序计数器,文件名和行号。
如果不可能恢复该信息,则ok布尔值为False。

go.uber.org/zap日志库为例,它也提供了设置该skip的设置,以方便对其进行二次包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lgcfg = zap.Config{
Level: zap.NewAtomicLevelAt(zap.DebugLevel),
Development: true,
Encoding: "console",
EncoderConfig: zap.NewDevelopmentEncoderConfig(),
OutputPaths: []string{"stderr", logFilePath},
ErrorOutputPaths: []string{"stderr"},
}
lgr, _ := lgcfg.Build(zap.AddCaller(), zap.AddCallerSkip(1))
logger = lgr.Sugar()

func Info(args ...interface{}) {
logger.Info(args...)
}

func Infof(tpl string, args ...interface{}) {
logger.Infof(tpl, args...)
}

椭圆曲线的选择

FileCoin和大多数主流区块链一样,底层使用secp256k1这条椭圆曲线。secp256k1有多种语言版本。

比特币纯Go版本实现:btcec

以太坊采用的C++版本:go-ethereum/crypto/secp256k1

以及FileCoin使用的C版本:go-secp256k1

💡 注意,FileCoin 支持两种签名,一种是椭圆曲线签名,一种是bls签名。

bls签名库使用了rust版本的实现:Filecoin Proofs FFI

BLS 签名算法是一种可以实现签名聚合和密钥聚合的算法(即可以将多个密钥聚合成一把密钥,将多个签名聚合成一个签名)。在以太坊未来的 Casper 实现中,有非常多的验证者都要对区块签名,要保证系统的安全性,同时节约存储空间,就需要用到这类签名聚合的算法。

Boneh-Lynn-Shacham

我们使用椭圆曲线签名来生成私钥。

生成私钥

以比特币btcec为例,生成符合FileCoin的私钥:

1
sk, _ := btcec.NewPrivateKey(btcec.S256())

生成地址

获得公钥

1
2
3
4
import "crypto/elliptic"

// 序列化椭圆曲线上的坐标点为未压缩格式的公钥
pub := elliptic.Marshal(btcec.S256(), sk.PublicKey.X, sk.PublicKey.Y)

获得地址

对公钥做哈希运算

FileCoin中有一个通用的哈希函数,使用blake2b快速哈希。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
func hash(ingest []byte, cfg *blake2b.Config) []byte {
hasher, err := blake2b.New(cfg)
if err != nil {
// If this happens sth is very wrong.
panic(fmt.Sprintf("invalid address hash configuration: %v", err)) // ok
}
if _, err := hasher.Write(ingest); err != nil {
// blake2bs Write implementation never returns an error in its current
// setup. So if this happens sth went very wrong.
panic(fmt.Sprintf("blake2b is unable to process hashes: %v", err)) // ok
}
return hasher.Sum(nil)
}

对公钥进行一次哈希运算:

1
hash(pub, &blake2b.Config{Size: 20})

原文注释里是这样说的:

PayloadHashLength defines the hash length taken over addresses using the Actor and SECP256K1 protocols.

这里的PayloadHashLength等于20。

生成地址格式

然后拼接地址格式:

1
2
3
4
5
explen := 1 + len(payload)
buf := make([]byte, explen)

buf[0] = protocol
copy(buf[1:], payload)

上面的protocol值固定为1,payloadhash(pub)

对地址编码

FileCoin 内有这样一个函数,将生成编码后我们看到的地址格式如:t1qecjxje6yjq2yatfgj3noapi5fa3cr7vmrw6xti

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
func encode(network Network, addr Address) (string, error) {
if addr == Undef {
return UndefAddressString, nil
}
var ntwk string
switch network {
case Mainnet:
ntwk = MainnetPrefix
case Testnet:
ntwk = TestnetPrefix
default:
return UndefAddressString, ErrUnknownNetwork
}

var strAddr string
switch addr.Protocol() {
case SECP256K1, Actor, BLS:
cksm := Checksum(append([]byte{addr.Protocol()}, addr.Payload()...))
strAddr = ntwk + fmt.Sprintf("%d", addr.Protocol()) + AddressEncoding.WithPadding(-1).EncodeToString(append(addr.Payload(), cksm[:]...))
case ID:
i, n, err := varint.FromUvarint(addr.Payload())
if err != nil {
return UndefAddressString, xerrors.Errorf("could not decode varint: %w", err)
}
if n != len(addr.Payload()) {
return UndefAddressString, xerrors.Errorf("payload contains additional bytes")
}
strAddr = fmt.Sprintf("%s%d%d", ntwk, addr.Protocol(), i)
default:
return UndefAddressString, ErrUnknownProtocol
}
return strAddr, nil
}

Network看作是枚举,0代表Mainnet1代表TestnetTestnetPrefix = “t”MainnetPrefix = “f”

对地址格式再次做哈希,作为地址摘要

1
cksm := hash(buf, &blake2b.Config{Size: 4})

ChecksumHashLength defines the hash length used for calculating address checksums.

ChecksumHashLength等于4。

hash(pub)+cksum做base32转换。

1
2
3
4
const encodeStd = "abcdefghijklmnopqrstuvwxyz234567"

// AddressEncoding defines the base32 config used for address encoding and decoding.
var AddressEncoding = base32.NewEncoding(encodeStd)

最终地址等于网络前缀+椭圆曲线类型+base32(hash(pub)+cksum)

离线签名

1
2
3
4
5
6
7
8
9
10
11
// 使用secp256k1签名
// 签名前使用blake2哈希算法对要签名的消息做信息摘要,统一签名消息的长度
func (secpSigner) Sign(pk []byte, msg []byte) ([]byte, error) {
b2sum := blake2b.Sum256(msg)
sig, err := crypto.Sign(pk, b2sum[:])
if err != nil {
return nil, err
}

return sig, nil
}

crypto.Sign

1
2
3
4
5
// Sign signs the given message, which must be 32 bytes long.
func Sign(sk, msg []byte) ([]byte, error) {
// secp256k1 就是最终的椭圆曲线签名,这个签名可以替换成secp256k1不同语言版本的实现
return secp256k1.Sign(msg, sk)
}

以下代码执行结果是什么

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
package main

import "fmt"

type Namespace interface {
Value() string
}

type MyNamespace struct {
value string
}

func (n *MyNamespace) Value() string {
return n.value
}

func show(n Namespace) {
if n != nil {
fmt.Println(n.Value())
} else {
fmt.Println("Hello World!")
}
}

func main() {
var n *MyNamespace = nil
show(n)
}

结果是报错。

1
2
3
4
5
6
7
8
9
10
11
12
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xe01c6]

goroutine 1 [running]:
main.(*MyNamespace).Value(0x0, 0x5bf2, 0x119bf8, 0x1d)
/tmp/sandbox404522411/prog.go:16 +0x6
main.show(0x15d910, 0x0)
/tmp/sandbox404522411/prog.go:21 +0x40
main.main()
/tmp/sandbox404522411/prog.go:30 +0x40

Program exited.

报一个空指针错误。n != nil的判断在这里无效。

原因是Go语言中,interface{}类型的变量包含了2个指针,一个指针指向值的类型,一个指针指向实际的值。MyNamespace实现了Namespace接口,n在传入Show(n)函数时被自动转型成为Namespacevar n *MyNameSpace = nil只是声明了该接口类型的实际值为nil,虽然我们把一个nil值赋值给它,但是实际上interface里依然存了指向类型的指针,所以拿这个interface变量去和nil常量进行比较的话就会返回false

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
type asStruct struct {
pt uintptr
pv uintptr
}

func show(n Namespace) {
fmt.Printf("👉👉 %p\n", n)
x := *(*asStruct)(unsafe.Pointer(&n))
spew.Dump(x)
spew.Println("n == nil ?", n == nil)
if n != nil && !reflect.ValueOf(n).IsNil() {
fmt.Println(n.Value())
} else {
fmt.Println("Hello World!")
}
}

// Output:
// 👉👉 0x0
// (main.asStruct) {
// pt: (uintptr) 0x1125f80,
// pv: (uintptr) <nil>
// }
// n == nil ? false
// Hello World!

我们看到传入的参数指针值为0,但interface{}类型的变量的两个指针,值的类型是有值的,实际值为nil。因此接口传入的不是实际的nil的情况下,n==nil就为false。可以使用reflect.ValueOf(n).IsNil()来判断是否实际值为nil。

reflect.ValueOf只接受指针类型,否则会panic

如果显式传入show(nil)interface{}类型变量的两个指针都为nil,则n == nil判断生效。

1
2
3
4
5
6
//show(nil)
(main.asStruct) {
pt: (uintptr) <nil>,
pv: (uintptr) <nil>
}
n == nil ? true

参考

Go语言第一深坑 - interface 与 nil 的比较

golang中interface判断nil问题