开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Rust的相关工具 Rust如何让程序员更优秀 Rustfm

发表于 2021-11-22

[这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战]


Rust如何让程序员更优秀

Rust 程序设计语言的本质在于 赋能(empowerment),通过严苛的约束提高编码能力。Rust语言可以使你无后顾之忧的走入“系统层面”(“systems-level”)的工作,接触内存管理、数据表示和并发等底层细节。在 Rust 中,编译器充当了守门员的角色,它拒绝编译存在这些难以捕获的 bug 的代码,这其中包括并发 bug。通过与编译器合作,团队将更多的时间聚焦在程序逻辑上,而不是追踪 bug。

而为了方便使用,Rust提供了一些相关工具帮助开发者编写代码。如下所示:

  • Rustfmt 可以确保开发者遵循一致的代码风格。
  • Cargo 是内置的依赖管理器和构建工具,可以增加、编译和管理依赖,并使依赖在 Rust 生态中拥有相同的文件结构。
  • Rust Language Server 为集成开发环境(IDE)提供了强大的代码补全和内联错误信息功能。

Rustfmt

rustfmt是自动格式化工具,它会根据社区代码风格格式化代码,很多项目使用 rustfmt 来避免编写 Rust 风格的争论。

如果想在 Rust 项目中坚持标准风格,也需要使用rustfmt以特定的风格格式化你的代码。常见的几个格式约束如下所示

  • Rust 风格是缩进四个空格,而不是一个制表符;
  • println!调用一个 Rust 宏。如果它改为调用函数,则输入为println(不带!)
  • 大多数 Rust 代码行都以分号结尾。

安装:

1
csharp复制代码rustup component add rustfmt

格式化命令:

1
bash复制代码cargo fmt

运行此命令会格式化当前 crate 中所有的 Rust 代码。这应该只会改变代码风格,而不是代码语义。

Cargo

Cargo 是 Rust 的构建系统和包管理器,可以用于构建代码、下载并编译依赖库。

相关命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码-- 查询版本
$ cargo --version
cargo 1.52.0 (69767412a 2021-04-21)

-- 构建
cargo build

--运行
cargo run

-- 快速检查代码,但并不产生可执行文件\
cargo check

-- 发布(release)构建
cargo build --release

release会在target/release 而不是 target/debug 下生成可执行文件。这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。

创建项目

1
2
go复制代码$ cargo new hello_
Created binary (application) `hello_` package

目录结构:

  • .git:git 仓库
  • src
    • main.rs
  • Cargo.toml: 使用 TOML (Tom’s Obvious, Minimal Language) 格式,这是 Cargo 的配置文件

Cargo.toml文件的默认内容:

1
2
3
4
5
6
7
8
9
ini复制代码[package]
name = "hello_"
version = "0.1.0"
authors = ["name <email>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
  • package 表示下面的语句用来配置包;
  • dependencies 罗列项目的依赖,当添加新依赖时需要添加到[dependencies]下。

编写并运行Rust 程序

项目会生成源文件名为 main.rs。Rust 源文件总是以 .rs 扩展名结尾。如果文件名包含多个单词,使用下划线分隔它们。例如命名为 hello_world.rs,而不是 helloworld.rs。

1
2
3
rust复制代码fn main() {
println!("Hello, world!");
}
  • main 函数是一个特殊的函数:在可执行的 Rust 程序中,它总是最先运行的代码。函数体被包裹在花括号{}中。Rust 要求所有函数体都要用花括号包裹起来。
  • println! 调用了一个 Rust 宏。如果是调用函数,则应输入 println (无需! );
  • 该行以分号结尾( ; ),这代表一个表达式的结束和下一个表达式的开始。

保存文件,并回到终端窗口。在 Linux 或 macOS 上,输入如下命令,编译并运行文件:

1
2
3
4
5
6
shell复制代码$ rustc main.rs
$ ./main
Hello, world!

window:
> .\main.exe

Rust Language Server (RLS)

RLS 提供了一个在后台运行的服务器,为 IDE、编辑器和其他工具提供有关 Rust 程序的信息。

安装

1
2
3
4
5
sql复制代码-- 更新rustup
rustup update


rustup component add rls rust-analysis rust-src

为vscode安装vscode-rust检测RLS是否配置成功。

image.png

可以看到下图中已经存在语法提示
image.png

参考资料

hello cargo

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

centos7下mysql8 主从安装配置 centos7下

发表于 2021-11-22

centos7下mysql8 主从安装配置

centos7 版本下载

镜像版本要求centos7的小版本;下载地址建议从阿里云镜像站下载地址
image.png
目前官方镜像分为如下类型:

  • CentOS-7-x86_64-DVD-2009.iso 标准安装版,一bai般下载这个就可以了。
  • CentOS-7-x86_64-NetInstall-2009.iso 网络安装镜像du。
  • CentOS-7-x86_64-Everything-2009.iso 对完整版安装盘的软件进行补充集成所有软
  • CentOS-7-x86_64-Minimal-2009.iso 精简版本,包含核心组件。
    这边我使用CentOS-7-x86_64-DVD-2009.iso 安装centos7操作系统

image.png

mysql 8.0.20下载

mysql版本要求 mysql8.0.20;下载地址建议从官方镜像站下载地址:mysql8.0.20。

mysql8.0.20 二进制安装

总体安装步骤概述如下:

  1. 操作系统等相关配置设置
  2. 安装依赖包
  3. 创建用户
  4. 修改配置文件、创建相关数据目录、日志目录等并授权
  5. 运行安装命令,启动数据库
  6. 配置环境变量、服务等(看需要)

操作系统配置和设置

查看CPU、内存、SSL版本、硬盘大小、关闭防火墙和selinux

查看物理CPU个数

1
2
sh复制代码[root@localhost ~]# cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
1

查看逻辑CPU的个数

1
2
sh复制代码[root@localhost ~]# cat /proc/cpuinfo| grep "processor"| wc -l
1

查看CPU信息(型号)

1
2
sh复制代码[root@localhost ~]# cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c
1 Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz

查看内存:

1
2
3
4
5
sh复制代码[root@localhost ~]# cat /proc/meminfo
MemTotal: 1863076 kB
MemFree: 81492 kB
MemAvailable: 48092 kB
......

查看ssl版本

1
2
sh复制代码[root@localhost ~]# openssl version
OpenSSL 1.0.2k-fips 26 Jan 2017

查看硬盘大小 尽量将/根目录占用所有磁盘目录的80% ;home目录50-100g即可

1
2
3
4
5
6
7
8
9
sh复制代码[root@localhost ~]# df -lh
Filesystem Size Used Avail Use% Mounted on
devtmpfs 898M 0 898M 0% /dev
tmpfs 910M 0 910M 0% /dev/shm
tmpfs 910M 9.6M 901M 2% /run
tmpfs 910M 0 910M 0% /sys/fs/cgroup
/dev/mapper/centos-root 17G 5.4G 12G 32% /
/dev/sda1 1014M 150M 865M 15% /boot
tmpfs 182M 0 182M 0% /run/user/0

关闭防火墙

1
2
sh复制代码systemctl stop firewalld.service
systemctl disable firewalld.service

修改selinux,将SELINUX=enforcing 改为SELINUX=disabled

1
2
3
4
5
6
7
8
9
10
11
12
13
sh复制代码[root@localhost ~]# vi /etc/selinux/config

# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
# enforcing - SELinux security policy is enforced.
# permissive - SELinux prints warnings instead of enforcing.
# disabled - No SELinux policy is loaded.
SELINUX=disabled
# SELINUXTYPE= can take one of three values:
# targeted - Targeted processes are protected,
# minimum - Modification of targeted policy. Only selected processes are protected.
# mls - Multi Level Security protection.
SELINUXTYPE=targeted
1
2
sh复制代码# 不重启机器生效
setenforce 0

修改文件限制等

1
2
3
4
sh复制代码[root@localhost ~]# vi /etc/security/limits.conf
hard nofile 65535
soft nofile 65535
ulimit -n 65535
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
sh复制代码[root@localhost ~]# vi /etc/sysctl.conf

# sysctl settings are defined through files in
# /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
#
# Vendors settings live in /usr/lib/sysctl.d/.
# To override a whole file, create a new file with the same in
# /etc/sysctl.d/ and put new settings there. To override
# only specific settings, add a file with a lexically later
# name in /etc/sysctl.d/ and put new settings there.
#
# For more information, see sysctl.conf(5) and sysctl.d(5).

fs.aio-max-nr = 1048576
fs.file-max = 6553600
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
kernel.sysrq = 0
kernel.core_uses_pid = 1
net.ipv4.tcp_syncookies = 1
kernel.msgmnb = 65536
kernel.msgmax = 65536
kernel.shmmax = 68719476736
kernel.shmall = 4294967296
net.ipv4.tcp_max_tw_buckets = 6000
net.ipv4.tcp_sack = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_wmem = 4096 16384 4194304
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.netdev_max_backlog = 262144
net.ipv4.tcp_max_orphans = 3276800
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_syn_retries = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_fin_timeout = 1
net.ipv4.tcp_keepalive_time = 30
net.ipv4.ip_local_port_range = 1024 65000


# 生效
sysctl – p

安装mysql组件包

1
2
3
4
arduino复制代码yum install -y epel-release wget
yum install -y glibc gcc gcc-c++ openssl-devel autoconf automake cmake bison make ncurses-devel numactl numactl-devel
yum install -y libtool-ltdl-devel** zlib* libxml* fiex**
yum install -y libaio libaio-devel libmcrypt libmcrypt-devel mcrypt mhash**

创建用户和用户文件夹

1
2
复制代码groupadd mysql
useradd -r -g mysql mysql

将下载好的安装包上传到服务器执行目录,也可以直接用wget下载软件包

1
url复制代码https://cdn.mysql.com/archives/mysql-8.0/mysql-8.0.20-linux-glibc2.12-x86_64.tar.xz**](https://cdn.mysql.com/archives/mysql-8.0/mysql-8.0.20-linux-glibc2.12-x86_64.tar.xz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sh复制代码# 解压
tar -xvf mysql-8.0.20-linux-glibc2.12-x86_64.tar.xz
# 移动到/user/local 目录
mv mysql-8.0.20-linux-glibc2.12-x86_64 /usr/local/mysql
# 添加环境变量
cat >>/etc/profile << EOF
export PATH=$PATH:/usr/local/mysql/bin
EOF
# 环境变量生效
source /etc/profile
# 创建mysql安装所需文件夹
mkdir -p /data/mysql/tmp
mkdir -p /data/mysql/data
mkdir -p /data/mysql/etc
mkdir -p /data/mysql/logs
# 授予文件夹mysql权限
chown -R mysql:mysql /data/mysql

修改配置文件

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
yml复制代码[mysqld]
########basic settings########
server-id = 112 #建议和服务器ip保持一致 例如192.168.1.66 则此处写66
port = 3306
user = mysql
character_set_server=utf8mb4
skip_name_resolve = 1
max_connections = 800
max_connect_errors = 1000
datadir = /data/mysql/data #根据实际情况修改,建议和程序分离存放
transaction_isolation = READ-COMMITTED
explicit_defaults_for_timestamp = 1
join_buffer_size = 512M
tmp_table_size = 512M
tmpdir = /data/mysql/tmp
pid-file = /data/mysql/tmp/mysqld.pi
socket = /tmp/mysql.sock
max_allowed_packet = 1024M
sql_mode = "STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO"
interactive_timeout = 1800
wait_timeout = 1800
read_buffer_size = 16M
read_rnd_buffer_size = 32M
sort_buffer_size = 32M
lower_case_table_names=1 #大小写不敏感,linux专属 window服务器直接注释掉
default_time_zone='+8:00'
########log settings########
log_error =/data/mysql/logs/mysqld.log #根据实际情况修改建议和datadir 位于不同物理磁盘
slow_query_log = 1
slow_query_log_file = /data/mysql/logs/slow.log #根据实际情况修改建议和datadir 位于不同物理磁盘
#log_queries_not_using_indexes = 1 #调试数据库的时候打开,此选项将记录所有没走索引的sql语句,不管执行快慢
log_slow_admin_statements = 1
log_slow_slave_statements = 1
#log_throttle_queries_not_using_indexes = 10 #设定每分钟记录到日志的未使用索引的语句数目,超过这个数目后只记录语句数量和花费的总时间
binlog_expire_logs_seconds=10800 #如果log_bin 所在磁盘空间充足;建议调整至25200
long_query_time = 2
min_examined_row_limit = 100


########replication settings########
master_info_repository = TABLE
relay_log_info_repository = TABLE
log_bin = /data/mysql/logs/mysql_binlog #根据实际情况修改建议和datadir 位于不同物理磁盘
sync_binlog = 1
gtid_mode = on
enforce_gtid_consistency = 1
log_slave_updates
binlog_format = row
relay_log = /data/mysql/logs/relay.log #根据实际情况修改建议和datadir 位于不同物理磁盘
relay_log_recovery = 1
binlog_gtid_simple_recovery = 1
slave_skip_errors = ddl_exist_errors

########innodb settings########
innodb_page_size = 16384 #兼容老系统
innodb_buffer_pool_size = 14G #根据实际情况修改 一般为服务器可用内存的80%
innodb_buffer_pool_instances = 8
innodb_buffer_pool_load_at_startup = 1
innodb_buffer_pool_dump_at_shutdown = 1
innodb_lru_scan_depth = 2000
innodb_lock_wait_timeout = 5
innodb_io_capacity = 4000
innodb_io_capacity_max = 8000
innodb_flush_method = O_DIRECT
innodb_log_group_home_dir = /data/mysql/logs/ #根据实际情况修改建议和datadir 位于不同物理磁盘
innodb_flush_neighbors = 1
innodb_log_file_size = 512M #根据实际情况修改
innodb_log_buffer_size = 16777216
innodb_purge_threads = 4
innodb_thread_concurrency = 64
innodb_print_all_deadlocks = 1
innodb_strict_mode = 1
innodb_sort_buffer_size = 67108864
innodb_read_io_threads=24 #服务器cpu逻辑核数的60% 取整数 例如你是32核 建议调整到19
innodb_write_io_threads=24 #服务器cpu逻辑核数的60% 取整数 例如你是32核 建议调整到19

########semi sync replication settings########
plugin_dir=/usr/local/mysql/lib/plugin #根据实际情况修改
plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
[mysqld-8.0]
innodb_buffer_pool_dump_pct = 40
innodb_page_cleaners = 4
innodb_undo_log_truncate = 1
innodb_purge_rseg_truncate_frequency = 128
binlog_gtid_simple_recovery=1
log_timestamps=system
transaction_write_set_extraction=MURMUR32
default_authentication_plugin = 'mysql_native_password'
log_bin_trust_function_creators=1

安装和启动数据库

指定本实例的配置文件进行安装

建议写全路径运行命令进行安装

1
sh复制代码/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf --initialize --user=mysql

查看日志

1
bash复制代码vi /data/mysql/logs/mysqld.log

此时可以看到临时密码,并查看是否有错误产生:A temporary password is generated for root@localhost: 密码

1
2
3
4
5
6
7
ini复制代码[root@localhost ~]# vi /data/mysql/logs/mysqld.log

2021-11-22T11:36:07.179120+08:00 0 [System] [MY-013169] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.20) initializing of server in progress as process 2503
2021-11-22T11:36:07.216846+08:00 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2021-11-22T11:36:11.364494+08:00 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
2021-11-22T11:36:11.697981+08:00 0 [Warning] [MY-013501] [Server] Ignoring --plugin-load[_add] list as the server is running with --initialize(-insecure).
2021-11-22T11:36:12.605450+08:00 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: VZico+qE8l0M

启动数据库

  • 方法1:以服务模式启动数据库 建议用此方式启动。
1
2
sh复制代码cp /usr/local/mysql/support-files/mysql.server  /etc/init.d/mysqld
service mysqld start
  • 启动数据库,并在后台运行(执行下面命令时多次回车,专为小白而备注)
1
bash复制代码/usr/local/mysql/bin/mysqld_safe  --defaults-file=/etc/my.cnf &

查看数据库进程

1
2
3
4
sh复制代码[root@localhost ~]# ps -ef | grep mysql
root 1901 1 0 15:59 pts/0 00:00:00 /bin/sh /usr/local/mysql/bin/mysqld_safe --datadir=/data/mysql/data --pid-file=/data/mysql/tmp/mysqld.pi
mysql 2800 1901 1 15:59 pts/0 00:00:56 /usr/local/mysql/bin/mysqld --basedir=/usr/local/mysql --datadir=/data/mysql/data --plugin-dir=/usr/local/mysql/lib/plugin --user=mysql --log-error=/data/mysql/logs/mysqld.log --pid-file=/data/mysql/tmp/mysqld.pi --socket=/tmp/mysql.sock --port=3306
root 5569 1816 0 16:54 pts/0 00:00:00 grep --color=auto mysql

修改mysql默认密码

登录数据库修改root密码。安装按成必须修改随机root密码,不然无法操作其他内容

1
2
sh复制代码[root@localhost ~]#/usr/local/mysql/bin/mysql -u root -p '初始生成密码' -S /tmp/mysql.sock
mysql> ALTER USER 'root'@'%' IDENTIFIED BY '要修改的密码';

mysql8主从配置

  1. 将上面安装好mysql的虚拟复制1台(注意修改ip)
  2. 修改master和slave my.cnf文件
1
2
sh复制代码#建议和服务器ip保持一致,防止重复
server-id = 112
  1. 重命名auto.cnf文件(auto.cnf默认在mysql的data目录下)
1
sh复制代码mv /data/mysql/data/auto.cnf /data/mysql/data/auto.cnf_bak
  1. 重启master服务
1
sh复制代码service mysqld restart
  1. 为从节点创建一个登陆用户 repl
1
2
3
4
5
6
7
sh复制代码[root@localhost ~]# mysql -u root -p
[这里输入mysql链接密码]
mysql> CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'Ron_master_1';
Query OK, 0 rows affected (0.01 sec)

mysql> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
Query OK, 0 rows affected (0.00 sec)

这里注意‘repl’@’%’中 % 不要写ip

  1. 查看master节点状态
1
2
3
4
5
6
7
8
9
10
sql复制代码[root@localhost ~]# mysql -u root -p
[这里输入mysql链接密码]
mysql> show master status;
+---------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| mysql_binlog.000010 | 1304 | | | 56b7da5a-4b45-11ec-ae40-000c2964986e:1-14,
fef0443e-4b5d-11ec-bf78-000c2964986e:1-5 |
+---------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

这里要注意 File 和 Position 后面要用。

  1. 登陆slave服务,配置主节点信息
1
2
3
4
5
6
7
8
sh复制代码[root@localhost ~]# mysql -u root -p
[这里输入mysql链接密码]
mysql> CHANGE MASTER TO
-> MASTER_HOST='192.168.182.110',
-> MASTER_USER='repl',
-> MASTER_PASSWORD='Ron_master_1',
-> MASTER_LOG_FILE='mysql_binlog',
-> MASTER_LOG_POS=856;

注意:

1
2
3
4
5
6
7
8
9
10
11
makefile复制代码CHANGE MASTER TO
# master 节点ip
MASTER_HOST='192.168.182.110,
# master节点登陆用户
MASTER_USER='repl',
# master节点登陆用户密码
MASTER_PASSWORD='Ron_master_1',
# binlog日志文件 对应上一步中的File
MASTER_LOG_FILE='mysql_binlog',
# 对应上一步Position
MASTER_LOG_POS=856;
  1. 检查slave节点状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码mysql> show slave status\G;
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.182.110
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql_binlog.000010
Read_Master_Log_Pos: 196
Relay_Log_File: relay.000002
Relay_Log_Pos: 327
Relay_Master_Log_File: mysql_binlog.000010
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:

Slave_IO_State: Waiting for master to send event
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
看到这些说明我们配置成功了,接下来可以使用nacicat等连接工具测试一下。

记录

  1. mysql查看当前用户列表
1
sql复制代码SELECT User, Host FROM mysql.user;

常见问题

  1. Fatal error: The slave I/O thread stops because master and slave have equal MySQL server UUIDs; these UUIDs must be different for replication to work.
    image.png
    这个错误提示。即主从架构中使用了相同的UUID。首先排查server_id系统变量,是否相同:
1
2
3
4
5
6
7
sql复制代码mysql> show variables like 'server_id';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id | 110 |
+---------------+-------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
sql复制代码mysql> show variables like 'server_id';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id | 111 |
+---------------+-------+
1 row in set (0.00 sec)

发现我们已经改为两个不同的了,查看auto.cnf文件(找不到可以使用find / -name auto.cnf查找)

1
2
3
4
sh复制代码[root@localhost logs]# vim /data/mysql/data/auto.cnf

[auto]
server-uuid=56b7da5a-4b45-11ec-ae40-000c2964986e
1
2
3
4
sh复制代码[root@localhost logs]# vim /data/mysql/data/auto.cnf

[auto]
server-uuid=56b7da5a-4b45-11ec-ae40-000c2964986e

可以看出两个计器上的server-uuid出现了重复,原因是克隆了虚拟机,只改server_id不行。

解决:重命名master auto.cnf文件后重启mysql

1
2
3
4
5
6
7
8
sh复制代码[root@localhost ~]# mv /data/mysql/data/auto.cnf /data/mysql/data/auto.cnf.bak
[root@localhost ~]# service mysqld restart
Shutting down MySQL.... SUCCESS!
Starting MySQL.... SUCCESS!
[root@localhost ~]# vim /data/mysql/data/auto.cnf

[auto]
server-uuid=fef0443e-4b5d-11ec-bf78-000c2964986e

重启后可以看出server-uuid已经重新生成了。

  1. 主从同步报错 Got fatal error 1236 from master when reading data from binary log: ‘Could not find first log…
    这个是因为binlog位置索引处的问题,解决方法:
1
2
3
4
5
6
7
8
ini复制代码1.打开主服务器,进入mysql
2.执行flush logs;# 这时主服务器会重新创建一个binlog文件;
3.在主服务上执行show master slave \G; # 记录 a 和 Position
4.来到从服务器的mysql;
5.stop slave;
6.change master to master_log_file='File',master_log_pos=Position; #这里的file和pos都是上面主服务器master显示的。
7.start slave;
8.show slave status \G; # 查看从表状态

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MongoDB 执行计划 mongodb性能分析方法:exp

发表于 2021-11-22

mongodb性能分析方法:explain()

explain输出结果说明

在执行类似于 db.collections.find().explain() 时只会输出(返回)

1
2
3
js复制代码queryPlanner 
与
serverInfo

在执行类似于 db.collections.find().explain() 时只会输出(返回)

1
2
3
js复制代码queryPlanner 
与
serverInfo

而在执行类似于 db.collections.find().explain("executionStats") 或 db.collections.find().explain(1) 时则会输出(返回)

1
2
3
4
5
js复制代码queryPlanner 
与
executionStats
与
serverInfo

queryPlanner

queryPlanner模式下并不会去真正进行query语句查询,而是针对query语句进行执行计划分析并选出winning plan。

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
js复制代码queryPlanner: queryPlanner的返回

queryPlanner.namespace:该值返回的是该query所查询的表

queryPlanner.indexFilterSet:针对该query是否有indexfilter

queryPlanner.winningPlan:查询优化器针对该query所返回的最优执行计划的详细内容。

queryPlanner.winningPlan.stage:最优执行计划的stage,这里返回是FETCH,可以理解为通过返回的index位置去检索具体的文档(stage有数个模式,将在后文中进行详解)。

queryPlanner.winningPlan.inputStage:用来描述子stage,并且为其父stage提供文档和索引关键字。

queryPlanner.winningPlan.stage的child stage,此处是IXSCAN,表示进行的是index scanning。

queryPlanner.winningPlan.keyPattern:所扫描的index内容,此处是did:1,status:1,modify_time: -1与scid : 1

queryPlanner.winningPlan.indexName:winning plan所选用的index。

queryPlanner.winningPlan.isMultiKey是否是Multikey,此处返回是false,如果索引建立在array上,此处将是true。

queryPlanner.winningPlan.direction:此query的查询顺序,此处是forward,如果用了.sort({modify_time:-1})将显示backward。

queryPlanner.winningPlan.indexBounds:winningplan所扫描的索引范围,如果没有制定范围就是[MaxKey, MinKey],这主要是直接定位到mongodb的chunck中去查找数据,加快数据读取。

queryPlanner.rejectedPlans:其他执行计划(非最优而被查询优化器reject的)的详细返回,其中具体信息与winningPlan的返回中意义相同,故不在此赘述。

executionStats

executionTimeMillis

executionStats.executionTimeMillis:指的是语句的执行整体时间

executionStats.executionStages.executionTimeMillis:该查询根据index去检索document获取nReturned条具体数据的时间

executionStats.executionStages.inputStage.executionTimeMillis:该查询扫描totalKeysExamined行index所用时间

nReturned、totalKeysExamined、totalDocsExamined

这些都是直观地影响到 executionTimeMillis,我们需要扫描的越少速度越快。

nReturne:该条查询返回的条目

totalKeysExamined:索引扫描条目

totalDocsExamined:文档扫描条目

对于一个查询,我们最理想的状态是:

nReturned=totalKeysExamined & totalDocsExamined=0:仅仅使用到了index,无需文档扫描

或

nReturned=totalKeysExamined=totalDocsExamined:正常index利用,无多余index扫描与文档扫描。

Stage

那么又是什么影响到了totalKeysExamined和totalDocsExamined?是stage的类型。类型列举如下:

COLLSCAN:全表扫描

IXSCAN:索引扫描

FETCH:根据索引去检索指定document

SHARD_MERGE:将各个分片返回数据进行merge

SORT:表明在内存中进行了排序

LIMIT:使用limit限制返回数

SKIP:使用skip进行跳过

IDHACK:针对_id进行查询

SHARDING_FILTER:通过mongos对分片数据进行查询

COUNT:利用db.coll.explain().count()之类进行count运算

COUNTSCAN:count不使用Index进行count时的stage返回

COUNT_SCAN:count使用了Index进行count时的stage返回

SUBPLA:未使用到索引的$or查询的stage返回

TEXT:使用全文索引进行查询时候的stage返回

PROJECTION:限定返回字段时候stage的返回

不希望看到包含如下的stage:

COLLSCAN(全表扫描)

SORT(使用sort但是无index)

不合理的SKIP

SUBPLA(未用到index的$or)

COUNTSCAN(不使用index进行count)

参考文章

MongoDB执行计划学习整理_LOUISLIAOXH的专栏-CSDN博客

mongodb .explain(‘executionStats’) 查询性能分析(转)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

go语言笔记 程序基础一

发表于 2021-11-22

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

最近在跟着小册学习go 语言原理与实践。因为没有go语言基础,就跟着菜鸟教程学习了下go的基础语法go语言基础-菜鸟教程。go语言基础笔记,会有两篇,这是第一篇。

下载安装go解释器

1
arduino复制代码https://golang.org/dl/

1 hello world

hello.go

1
2
3
4
5
6
7
8
golang复制代码package main

import "fmt"

func main() {
/* 这是我的第一个简单的程序 */
fmt.Println("Hello, World!")
}
  • 运行程序
1
2
3
4
5
shell复制代码# 方式一
go run hello.go
# 方式二
go bulid hello.go
./hello.exe
  • 编译到不同操作系统的运行程序

    • mac

      1
      2
      go复制代码$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build test.go
      $ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build test.go
    • linux

      1
      2
      go复制代码$ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build test.go
      $ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build test.go
    • windows

      1
      2
      sql复制代码$ SET CGO_ENABLED=0SET GOOS=darwin3 SET GOARCH=amd64 go build test.go
      $ SET CGO_ENABLED=0 SET GOOS=linux SET GOARCH=amd64 go build test.go

2 数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main

import "fmt"

func main() {
var a bool = true //布尔
var b int = 123 // 整形
var c float32 = 123.123 //32位浮点型
// var d byte = 1 //字节
var ip *int //指针
var f string //字符
ip = &b

f = "菜鸟教程"
fmt.Print(a, b, c, ip, f)
}

result

1
2
3
4
5
go复制代码true 
123
123.123
0xc000012088
菜鸟教程

3 变量

  • 变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main
import "fmt"
func main() {

// 声明一个变量并初始化
var a = "RUNOOB"
fmt.Println(a)

// 没有初始化就为零值
var b int
fmt.Println(b)

// bool 零值为 false
var c bool
fmt.Println(c)
}

result

1
2
3
arduino复制代码RUNOOB
0
false

4 常量

  • const
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码package main

import "fmt"

func main() {
const LENGTH int = 10
const WIDTH int = 5
var area int
const a, b, c = 1, false, "str" //多重赋值

area = LENGTH * WIDTH
fmt.Printf("面积为 : %d", area)
println()
println(a, b, c)
}

result

1
2
rust复制代码面积为 : 50
1 false str
  • 常量表达式中的内置函数
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码package main

import "unsafe"
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)

func main(){
println(a, b, c)
}

result

1
复制代码abc 3 16
  • iota常量计数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码// 实例一
package main

import "fmt"

func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}

result

1
复制代码0 1 2 ha ha 100 100 7 8

5 运算符

  • 算术运算符
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
go复制代码package main

import "fmt"

func main() {

var a int = 21
var b int = 10
var c int

c = a + b
fmt.Printf("第一行 - c 的值为 %d\n", c )
c = a - b
fmt.Printf("第二行 - c 的值为 %d\n", c )
c = a * b
fmt.Printf("第三行 - c 的值为 %d\n", c )
c = a / b
fmt.Printf("第四行 - c 的值为 %d\n", c )
c = a % b
fmt.Printf("第五行 - c 的值为 %d\n", c )
a++
fmt.Printf("第六行 - a 的值为 %d\n", a )
a=21 // 为了方便测试,a 这里重新赋值为 21
a--
fmt.Printf("第七行 - a 的值为 %d\n", a )
}

result

1
2
3
4
5
6
7
r复制代码第一行 - c 的值为 31
第二行 - c 的值为 11
第三行 - c 的值为 210
第四行 - c 的值为 2
第五行 - c 的值为 1
第六行 - a 的值为 22
第七行 - a 的值为 20
  • 比较运算符
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
go复制代码package main

import "fmt"

func main() {
var a int = 21
var b int = 10

if( a == b ) {
fmt.Printf("第一行 - a 等于 b\n" )
} else {
fmt.Printf("第一行 - a 不等于 b\n" )
}
if ( a < b ) {
fmt.Printf("第二行 - a 小于 b\n" )
} else {
fmt.Printf("第二行 - a 不小于 b\n" )
}

if ( a > b ) {
fmt.Printf("第三行 - a 大于 b\n" )
} else {
fmt.Printf("第三行 - a 不大于 b\n" )
}
/* Lets change value of a and b */
a = 5
b = 20
if ( a <= b ) {
fmt.Printf("第四行 - a 小于等于 b\n" )
}
if ( b >= a ) {
fmt.Printf("第五行 - b 大于等于 a\n" )
}
}

result

1
2
3
4
5
css复制代码第一行 - a 不等于 b
第二行 - a 不小于 b
第三行 - a 大于 b
第四行 - a 小于等于 b
第五行 - b 大于等于 a
  • 逻辑运算符
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
go复制代码package main

import "fmt"

func main() {
var a bool = true
var b bool = false
if ( a && b ) {
fmt.Printf("第一行 - 条件为 true\n" )
}
if ( a || b ) {
fmt.Printf("第二行 - 条件为 true\n" )
}
/* 修改 a 和 b 的值 */
a = false
b = true
if ( a && b ) {
fmt.Printf("第三行 - 条件为 true\n" )
} else {
fmt.Printf("第三行 - 条件为 false\n" )
}
if ( !(a && b) ) {
fmt.Printf("第四行 - 条件为 true\n" )
}
}

result

1
2
3
arduino复制代码第二行 - 条件为 true
第三行 - 条件为 false
第四行 - 条件为 true
  • 位运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码package main

import "fmt"

func main() {

var a uint = 60 /* 60 = 0011 1100 */
var b uint = 13 /* 13 = 0000 1101 */
var c uint = 0

c = a & b /* 12 = 0000 1100 */
fmt.Printf("第一行 - c 的值为 %d\n", c )

c = a | b /* 61 = 0011 1101 */
fmt.Printf("第二行 - c 的值为 %d\n", c )

c = a ^ b /* 49 = 0011 0001 */
fmt.Printf("第三行 - c 的值为 %d\n", c )

c = a << 2 /* 240 = 1111 0000 */
fmt.Printf("第四行 - c 的值为 %d\n", c )

c = a >> 2 /* 15 = 0000 1111 */
fmt.Printf("第五行 - c 的值为 %d\n", c )

result

1
2
3
4
5
r复制代码第一行 - c 的值为 12
第二行 - c 的值为 61
第三行 - c 的值为 49
第四行 - c 的值为 240
第五行 - c 的值为 15
  • 赋值运算符
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
38
39
40
41
go复制代码package main

import "fmt"

func main() {
var a int = 21
var c int

c = a
fmt.Printf("第 1 行 - = 运算符实例,c 值为 = %d\n", c )

c += a
fmt.Printf("第 2 行 - += 运算符实例,c 值为 = %d\n", c )

c -= a
fmt.Printf("第 3 行 - -= 运算符实例,c 值为 = %d\n", c )

c *= a
fmt.Printf("第 4 行 - *= 运算符实例,c 值为 = %d\n", c )

c /= a
fmt.Printf("第 5 行 - /= 运算符实例,c 值为 = %d\n", c )

c = 200;

c <<= 2
fmt.Printf("第 6行 - <<= 运算符实例,c 值为 = %d\n", c )

c >>= 2
fmt.Printf("第 7 行 - >>= 运算符实例,c 值为 = %d\n", c )

c &= 2
fmt.Printf("第 8 行 - &= 运算符实例,c 值为 = %d\n", c )

c ^= 2
fmt.Printf("第 9 行 - ^= 运算符实例,c 值为 = %d\n", c )

c |= 2
fmt.Printf("第 10 行 - |= 运算符实例,c 值为 = %d\n", c )

}

result

1
2
3
4
5
6
7
8
9
10
r复制代码第 1 行 - =  运算符实例,c 值为 = 21
第 2 行 - += 运算符实例,c 值为 = 42
第 3 行 - -= 运算符实例,c 值为 = 21
第 4 行 - *= 运算符实例,c 值为 = 441
第 5 行 - /= 运算符实例,c 值为 = 21
第 6行 - <<= 运算符实例,c 值为 = 800
第 7 行 - >>= 运算符实例,c 值为 = 200
第 8 行 - &= 运算符实例,c 值为 = 0
第 9 行 - ^= 运算符实例,c 值为 = 2
第 10 行 - |= 运算符实例,c 值为 = 2
  • 其他运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码package main

import "fmt"

func main() {
var a int = 4
var b int32
var c float32
var ptr *int

/* 运算符实例 */
fmt.Printf("第 1 行 - a 变量类型为 = %T\n", a );
fmt.Printf("第 2 行 - b 变量类型为 = %T\n", b );
fmt.Printf("第 3 行 - c 变量类型为 = %T\n", c );

/* & 和 * 运算符实例 */
ptr = &a /* 'ptr' 包含了 'a' 变量的地址 */
fmt.Printf("a 的值为 %d\n", a);
fmt.Printf("*ptr 为 %d\n", *ptr);
}

result

1
2
3
4
5
css复制代码第 1 行 - a 变量类型为 = int
第 2 行 - b 变量类型为 = int32
第 3 行 - c 变量类型为 = float32
a 的值为 4
*ptr 为 4
  • 运算符优先级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import "fmt"

func main() {
var a int = 20
var b int = 10
var c int = 15
var d int = 5
var e int;

e = (a + b) * c / d; // ( 30 * 15 ) / 5
fmt.Printf("(a + b) * c / d 的值为 : %d\n", e );

e = ((a + b) * c) / d; // (30 * 15 ) / 5
fmt.Printf("((a + b) * c) / d 的值为 : %d\n" , e );

e = (a + b) * (c / d); // (30) * (15/5)
fmt.Printf("(a + b) * (c / d) 的值为 : %d\n", e );

e = a + (b * c) / d; // 20 + (150/5)
fmt.Printf("a + (b * c) / d 的值为 : %d\n" , e );
}

result

1
2
3
4
r复制代码(a + b) * c / d 的值为 : 90
((a + b) * c) / d 的值为 : 90
(a + b) * (c / d) 的值为 : 90
a + (b * c) / d 的值为 : 50

6 条件语句

  • if else 语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

import "fmt"

func main() {
/* 局部变量定义 */
var a int = 100;

/* 判断布尔表达式 */
if a < 20 {
/* 如果条件为 true 则执行以下语句 */
fmt.Printf("a 小于 20\n" );
} else {
/* 如果条件为 false 则执行以下语句 */
fmt.Printf("a 不小于 20\n" );
}
fmt.Printf("a 的值为 : %d\n", a);

}

result

1
2
css复制代码a 不小于 20
a 的值为 : 100
  • switch 语句
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
go复制代码//实例一
package main

import "fmt"

func main() {
/* 定义局部变量 */
var grade string = "B"
var marks int = 90

switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}

switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}
fmt.Printf("你的等级是 %s\n", grade );
}

result

1
2
css复制代码优秀!
你的等级是 A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码//实例二 Type Switch
package main

import "fmt"

func main() {
var x interface{}

switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T",i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型" )
default:
fmt.Printf("未知型")
}
}

result

1
ruby复制代码x 的类型 :<nil>
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
go复制代码//实例三 fallthrough
package main

import "fmt"

func main() {

switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true")
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")
fallthrough
case true:
fmt.Println("4、case 条件语句为 true")
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}
}

result

1
2
3
arduino复制代码2、case 条件语句为 true
3、case 条件语句为 false
4、case 条件语句为 true
  • select语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package main

import "fmt"

func main() {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
}

result

1
perl复制代码no communication

7 循环语句

  • for 循环
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码//实例一
package main

import "fmt"

func main() {
sum := 0
for i := 0; i <= 10; i++ {
sum += i
}
fmt.Println(sum)
}

result

1
复制代码55
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码//实例二
package main

import "fmt"

func main() {
sum := 1
for ; sum <= 10; {
sum += sum
}
fmt.Println(sum)

// 这样写也可以,更像 While 语句形式
for sum <= 10{
sum += sum
}
fmt.Println(sum)
}

result

1
2
复制代码16
16
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码//实例三 无线循环
package main

import "fmt"

func main() {
sum := 0
for {
sum++ // 无限循环下去
}
fmt.Println(sum) // 无法输出
}

result

1
复制代码
  • break 终止
  • continue 跳过当前循环,进行下一轮
  • goto 转到标记处
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码// 实例 goto 
package main

import "fmt"

func main() {
/* 定义局部变量 */
var a int = 10

/* 循环 */
LOOP: for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto LOOP
}
fmt.Printf("a的值为 : %d\n", a)
a++
}
}

result

1
2
3
4
5
6
7
8
9
less复制代码a的值为 : 10
a的值为 : 11
a的值为 : 12
a的值为 : 13
a的值为 : 14
a的值为 : 16
a的值为 : 17
a的值为 : 18
a的值为 : 19

8 语言函数

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
go复制代码package main

import "fmt"

func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
var ret int

/* 调用函数并返回最大值 */
ret = max(a, b)

fmt.Printf( "最大值是 : %d\n", ret )
}

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 定义局部变量 */
var result int

if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}

result

1
复制代码最大值是 : 200
  • 返回多个值
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码package main

import "fmt"

func swap(x, y string) (string, string) {
return y, x
}

func main() {
a, b := swap("Google", "Runoob")
fmt.Println(a, b)
}

result

1
复制代码Runoob Google

9 语言变量作用域

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
go复制代码package main

import "fmt"

/* 声明全局变量 */
var a int = 20;

func main() {
/* main 函数中声明局部变量 */
var a int = 10
var b int = 20
var c int = 0

fmt.Printf("main()函数中 a = %d\n", a);
c = sum( a, b);
fmt.Printf("main()函数中 c = %d\n", c);
}

/* 函数定义-两数相加 */
func sum(a, b int) int {
fmt.Printf("sum() 函数中 a = %d\n", a);
fmt.Printf("sum() 函数中 b = %d\n", b);

return a + b;
}

result

1
2
3
4
ini复制代码main()函数中 a = 10
sum() 函数中 a = 10
sum() 函数中 b = 20
main()函数中 c = 30

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

详解在Linux中安装配置MongoDB

发表于 2021-11-22

最近在整理自己私人服务器上的各种阿猫阿狗,正好就顺手详细记录一下清理之后重装的步骤,今天先写点数据库的内容,关于在Linux中安装配置MongoDB

说实话为什么会装MongoDB呢,因为之前因为公司需要做点Nodejs的中间件,我顺手玩了一下MongoDB的CRUD,文档型数据库还是挺有意思的

安装环境

CentOS7 + MongoDB4.4

下载安装包

mongodb-4.4.4 版本下载地址(点击链接直接下载)

操作步骤

  1. 利用 xFtp 上传 mongodb.gz 包至安装目录下,我的目录是 /usr/soft/sort
  2. 解压安装包至指定目录下,我的是同目录下的install文件夹

tar -zxvf /usr/soft/sort/mongodb-linux-x86_64-rhel70-4.4.4.tgz -C /usr/soft/install/
3. 配置环境变量

vim /etc/profile
4. 在文件的最后一行添加如下内容

按 i 开始修改(注意 s 会删除当前选中字符)

export PATH=$PATH:/usr/soft/install/mongodb-linux-x86_64-rhel70-4.4.4/bin

按 esc 停止编辑,按 : 开始输入,输入 wq 保存并退出

之前都会用一个别名来拼接地址,其实直接写完整地址也可以,$PATH 应该是代指之前存有的 PATH变量
5. 输入 source /etc/profile ,无报错立即生效
6. 创建数据存放文件夹和日志记录文件夹,为后面的配置文件使用

在主目录下创建 /data/db 来存放数据

在主目录下创建 logs 来存放日志
7. 创建运行时使用的配置文件

在主目录下进入bin目录 cd /bin

创建配置文件 vim mongodb.conf

输入以下配置(一定要写完整地址,教程上面是相对地址,结果我启动的时候一直报配置错误)

dbpath = /usr/soft/install/mongodb-linux-x86_64-rhel70-4.4.4/data/db # 数据文件存放目录

logpath = /usr/soft/install/mongodb-linux-x86_64-rhel70-4.4.4/logs # 日志文件存放目录

port = 27017 # 端口

fork = true # 以守护程序的方式启用,即在后台运行

# auth=true # 需要认证,如果放开注释,就必须创建MongoDB的账号,使用账号与密码才可远程访问,第一次安装建议注释

bind_ip = 0.0.0.0 # 允许远程访问,或者直接注释,127.0.0.1是只允许本地访问

注意如果不创建账号,是可以直连数据库的,但是创建了账号之后是不能直连的必须要带账号密码才可以连接,例如下面这样

mongodb://root:******@xxx.xxx.xxx.xxx:27017/test?authSource=admin&readPreference=primary&ssl=false

问号后面内容后期了解清楚,之前不加一直无法连接上

注意:注释符号 # 和数据之间必须是一个空格
8. 测试运行和关闭数据库

在主目录下进入bin目录 cd /bin

启动 ./mongod -f mongodb.conf

关闭 pkill mongod(教程介绍了三种方法,目前我只有这一种命令成功了)

检查端口是否已经被占用 netstat -nltp|grep 27017 或者 top
9. 相关错误提示

child process failed,existed with error number 1 之类的错误是配置文件写错,之前就是相对地址而不是全地址导致一直报这个错没有成功运行

Mongodb enable authentication 开启了权限或者是创建了账户密码,就需要使用用户名密码连接登录,裸连会直接报这个没有权限的错误

参考资料一 ———— Linux安装、运行MongoDB

参考资料二 ———— 在Linux服务器中配置mongodb环境的步骤

参考资料三 ———— ERROR: child process failed, exited with error number 1

我是 fx67ll.com,如果您发现本文有什么错误,欢迎在评论区讨论指正,感谢您的阅读!

如果您喜欢这篇文章,欢迎访问我的 本文github仓库地址,为我点一颗Star,Thanks~ :)

转发请注明参考文章地址,非常感谢!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

详解在Linux中安装配置MySQL

发表于 2021-11-22

最近在整理自己私人服务器上的各种阿猫阿狗,正好就顺手详细记录一下清理之后重装的步骤,今天先写点数据库的内容,关于在Linux中安装配置MySQL

安装环境

CentOS7 + MySQL5.7

下载安装包

mysql-5.7.26 版本下载地址(点击链接直接下载)

操作步骤

  1. 利用 xFtp 上传 mysql.gz 包至安装目录下,我的目录是 /usr/soft/sort
  2. 解压安装包至指定目录下,我的是同目录下的install文件夹

tar -zxvf /usr/soft/sort/mysql-5.7.26-linux-glibc2.12-x86_64.tar.gz -C /usr/soft/install/
3. 创建组

groupadd mysql
4. 创建用户

useradd -r -g mysql mysql
5. 将安装目录所有者及所属组改为mysql

chown -R mysql.mysql /usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64
6. 进入mysql目录并创建data文件夹用于存放数据库表之类的数据

cd /usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64

mkdir data
7. 准备初始化,首先要安装依赖库libaio

yum install libaio
8. 准备初始化,这一步务必记住初始密码,它位于输出日志的末尾(数据库管理员临时密码)

注意这是一整条命令:/usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64/bin/mysqld --user=mysql --basedir=/usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64/ --datadir=/usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64/data --initialize

我的输出日志示例:20xx-xx-xxTxx:xx:xx.493483Z 1 [Note] A temporary password is generated for root@localhost: 这里是初始的临时密码
9. 配置系统环境变量
* 编辑 vim /etc/profile
* 添加以下环境变量

> `export MYSQL_HOME=/usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64`  
> 
> `export PATH=$PATH:$MYSQL_HOME/bin`
* 更新 `source /etc/profile`
  1. 配置mysql配置,这里最好查询一下所有配置的含义,可以参考 这篇文章

datadir=/usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64/data

basedir=/usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64

socket=/tmp/mysql.sock(这行很重要,不然后续socket连接会出问题)

user=mysql

port=3306

innodb_file_ per_table=1

character-set-server=utf8
11. 这里需要操作两个目录,用于配置文件中部分文件的运行,不然直接启动会报错,建议先完成错误解决方案中的代码
* 第一个错误mysqld_safe error: log-error set to /var/log/mariadb/mariadb.log
* 第一个错误解决方案,新建并添加权限

> `mkdir /var/log/mariadb`  
> 
> `touch /var/log/mariadb/mariadb.log`  
> 
> `chown -R mysql:mysql /var/log/mariadb/`
* 第二个错误`mysqld_safe Directory '/var/lib/mysql' for UNIX socket file don't exists.`
* 第二个错误解决方案,新建并添加权限

> `mkdir /var/lib/mysql`  
> 
> `chmod 777 /var/lib/mysql`
* [参考文档一](https://blog.csdn.net/qq_34218345/article/details/106951035)
* [参考文档二](https://blog.csdn.net/qq_32331073/article/details/76229420)
  1. 将mysql加入服务

cp /usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64/support-files/mysql.server /etc/init.d/mysql
13. 设置开机启动

chkconfig mysql on
14. 添加软连接

ln -s /usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64/support-files/mysql.server /etc/init.d/mysql

ln -s /usr/soft/install/mysql-5.7.26-linux-glibc2.12-x86_64/bin/mysql /usr/bin/mysql
15. 启动mysql

service mysql start
16. 使用初始密码登录
* 执行 mysql -u root -p(socket连接)
* 或者执行 mysql -u root -h 127.0.0.1 -p(本地连接)
* 输入密码,可以直接去前面保存的初始密码复制过来
17. 修改初始密码
* use mysql;(注意mysql语句使用英文;结束!!!)
* mysql> update user set authentication_string=passworD("你的新密码") where user='root';(mysql5.7及以上版本需要使用authentication_string字段来修改密码,有些博文并未提及,需要注意!!!)
* 这个也可修改密码,效果同上 set password=password("你的新秘密");
* 重新加载权限表 flush privileges;
* 退出mysql exit;

参考资料一 ———— linux下mysql的安装与使用

参考资料二 ———— linux 安装 mysql简单教程

参考资料三 ———— linux下mysql配置文件my.cnf详解

参考资料四 ———— 启动mysql报错mysqld_safe error: log-error set to /var/log/mariadb/mariadb.log

参考资料五 ———— mysqld_safe Directory ‘/var/lib/mysql‘ for UNIX socket file don‘t exists.

参考资料六 ———— linux下将mysql加入到环境变量

参考资料七 ———— MySQL–启动和关闭MySQL服务

参考资料八 ———— mysql报错:You must reset your password using ALTER USER statement before executing this statement.

参考资料九 ———— Linux下修改Mysql密码的三种方式

参考资料十 ———— 查看MySQL是否在运行

我是 fx67ll.com,如果您发现本文有什么错误,欢迎在评论区讨论指正,感谢您的阅读!

如果您喜欢这篇文章,欢迎访问我的 本文github仓库地址,为我点一颗Star,Thanks~ :)

转发请注明参考文章地址,非常感谢!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flink入门到实战-快速上手

发表于 2021-11-22

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

Flink的特点

  • 事件驱动(Event-driven)
  • 基于流处理

一切皆由流组成,离线数据是有界的流;实时数据是一个没有界限的流。(有界流、无界流)

  • 分层API
+ 越顶层越抽象,表达含义越简明,使用越方便
+ 越底层越具体,表达能力越丰富,使用越灵活

Flink vs Spark Streaming

  • 数据模型
    • Spark采用RDD模型,spark streaming的DStream实际上也就是一组组小批数据RDD的集合
    • flink基本数据模型是数据流,以及事件(Event)序列
  • 运行时架构
    • spark是批计算,将DAG划分为不同的stage,一个完成后才可以计算下一个
    • flink是标准的流执行模式,一个事件在一个节点处理完后可以直接发往下一个节点处理

快速上手

批处理实现WordCount

flink-streaming-java_2.12:1.12.1 => org.apache.flink:flink-runtime_2.12:1.12.1 => com.typesafe.akka:akka-actor_2.12:2.5.21,akka就是用scala实现的。即使这里我们用java语言,还是用到了scala实现的包

pom依赖

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.caicai</groupId>
<artifactId>Flink</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<flink.version>1.12.0</flink.version>
<scala.binary.version>2.14</scala.binary.version>
</properties>

<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
</dependencies>

</project>

准备工作

首先准备一个文件,存放一些简单的数据,以便后续Flink计算分析。在resources目录下新建一个hello.txt文件,并存入一些数据

1
2
3
4
5
6
7
8
txt复制代码hello java
hello flink
hello scala
hello spark
hello storm
how are you
fine thank you
and you

代码实现

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
38
39
40
41
42
43
44
45
46
java复制代码package com.caicai;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.operators.Order;
import org.apache.flink.api.java.DataSet;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.util.Collector;

/**
* @author wangcaicai
* @date 2021/11/18 0:51
* @description 批处理
*/
public class WordCount {
public static void main(String[] args) throws Exception {
//创建执行环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

//从文件中读取数据
String inputPath = "E:\\java\\WorkSpace\\Flink\\src\\main\\resources\\hello.txt";
DataSet<String> inputDataSet = env.readTextFile(inputPath);

// 对数据集进行处理,按空格分词展开,转换成(word, 1)二元组进行统计
// 按照第一个位置的word分组
// 按照第二个位置上的数据求和
DataSet<Tuple2<String, Integer>> resultSet = inputDataSet.flatMap(new MyFlatMapper())
.groupBy(0)
.sum(1);
resultSet.print();
}

// 自定义类,实现FlatMapFunction接口
public static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String, Integer>> {

@Override
public void flatMap(String s, Collector<Tuple2<String, Integer>> out) throws Exception {
// 按空格分词
String[] words = s.split(" ");
// 遍历所有word,包成二元组输出
for (String str : words) {
out.collect(new Tuple2<>(str, 1));
}
}
}
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
txt复制代码(thank,1)
(spark,1)
(and,1)
(java,1)
(storm,1)
(flink,1)
(fine,1)
(you,3)
(scala,1)
(are,1)
(how,1)
(hello,5)

解决 Flink 升级1.12 报错 No ExecutorFactory found to execute the application

流处理实现

在2.1批处理的基础上,新建一个类进行改动。

  • 批处理=>几组或所有数据到达后才处理;流处理=>有数据来就直接处理,不等数据堆叠到一定数量级
  • 这里不像批处理有groupBy => 所有数据统一处理,而是用流处理的keyBy => 每一个数据都对key进行hash计算,进行类似分区的操作,来一个数据就处理一次,所有中间过程都有输出!
  • 并行度:开发环境的并行度默认就是计算机的CPU逻辑核数

代码实现

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码package com.caicai;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

/**
* @author wangcaicai
* @date 2021/11/18 0:51
* @description 流处理
*/
public class StreamWordCount {
public static void main(String[] args) throws Exception {
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 设置并行度,默认值 = 当前计算机的CPU逻辑核数(设置成1即单线程处理)
// env.setMaxParallelism(4);

//从文件中读取数据
String inputPath = "E:\\java\\WorkSpace\\Flink\\src\\main\\resources\\hello.txt";
DataStream<String> inputDataStream = env.readTextFile(inputPath);

// 对数据集进行处理,按空格分词展开,转换成(word, 1)二元组进行统计
// 按照第一个位置的word分组
// 按照第二个位置上的数据求和
DataStream<Tuple2<String, Integer>> resultStream = inputDataStream.flatMap(new MyFlatMapper())
.keyBy(item -> item.f0)
.sum(1);
resultStream.print();

//执行任务
env.execute();
}

// 自定义类,实现FlatMapFunction接口
public static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String, Integer>> {

@Override
public void flatMap(String s, Collector<Tuple2<String, Integer>> out) throws Exception {
// 按空格分词
String[] words = s.split(" ");
// 遍历所有word,包成二元组输出
for (String str : words) {
out.collect(new Tuple2<>(str, 1));
}
}
}
}

输出:

这里因为是流处理,所以所有中间过程都会被输出,前面的序号就是并行执行任务的线程编号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
txt复制代码9> (how,1)
1> (scala,1)
6> (storm,1)
6> (are,1)
4> (hello,1)
4> (hello,2)
2> (java,1)
4> (hello,3)
1> (spark,1)
7> (you,1)
10> (flink,1)
4> (hello,4)
4> (hello,5)
7> (fine,1)
7> (you,2)
5> (thank,1)
11> (and,1)
7> (you,3)

流式数据源测试

  1. 通过nc -lk <port>打开一个socket服务,用于模拟实时的流数据
1
shell复制代码nc -lk 7777
  1. 代码修改inputStream的部分
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码package com.caicai;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

/**
* @author wangcaicai
* @date 2021/11/18 0:51
* @description 流处理
*/
public class StreamWordCount {
public static void main(String[] args) throws Exception {
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 设置并行度,默认值 = 当前计算机的CPU逻辑核数(设置成1即单线程处理)
// env.setMaxParallelism(32);

//从文件中读取数据
// String inputPath = "E:\\java\\WorkSpace\\Flink\\src\\main\\resources\\hello.txt";
// DataStream<String> inputDataStream = env.readTextFile(inputPath);

// 从socket文本流读取数据
DataStream<String> inputDataStream = env.socketTextStream("192.168.200.130", 7777);

// 对数据集进行处理,按空格分词展开,转换成(word, 1)二元组进行统计
// 按照第一个位置的word分组
// 按照第二个位置上的数据求和
DataStream<Tuple2<String, Integer>> resultStream = inputDataStream.flatMap(new MyFlatMapper())
.keyBy(item -> item.f0)
.sum(1);
resultStream.print();

//执行任务
env.execute();
}

// 自定义类,实现FlatMapFunction接口
public static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String, Integer>> {

@Override
public void flatMap(String s, Collector<Tuple2<String, Integer>> out) throws Exception {
// 按空格分词
String[] words = s.split(" ");
// 遍历所有word,包成二元组输出
for (String str : words) {
out.collect(new Tuple2<>(str, 1));
}
}
}
}
  1. 在本地开启的socket中输入数据,观察IDEA的console输出。

本人测试后发现,同一个字符串,前面输出的编号是一样的,因为key => hashcode,同一个key的hash值固定,分配给相对应的线程处理。

优化修改

上面的代码,我们是把host和port写死在代码中的,这样其实不太好,我们可以设置在参数(args)中,借助parameter tool工具提取这些配置项

1
2
3
4
5
6
7
8
java复制代码//改动部分
import org.apache.flink.api.java.utils.ParameterTool;
// 用parameter tool工具从程序启动参数中提取配置项
ParameterTool parameterTool = ParameterTool.fromArgs(args);
String host = parameterTool.get("host");
int port = parameterTool.getInt("port");
// 从socket文本流读取数据
DataStream<String> inputDataStream = env.socketTextStream(host, port);

将配置项设置在args参数中

输入

1
shell复制代码--host 192.168.200.130 --port 7777

然后点击右下角的apply,再次运行程序就可以了

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go错误处理

发表于 2021-11-22
  • 在项目开发中,我经常头疼的一个事情就是错误处理,主要体现在两个点
+ 写代码时调用其他函数,它返回了一个error,该如何处理?


    - 我以往都是先`log`再`return`。然后这个A函数被B函数调用了,B函数又一样的处理方式,最终导致日志一大堆重复信息,而且还没有堆栈。
+ 自己写函数,出现一种非正常情况,我该怎么定义error?


    - 一般都随心所欲`return errors.New("一堆文字")`。调用方几乎很难判断错误的原因。
  • 为了解决问题,参考了毛剑老师的分享和很多业内大佬的文章,越来越意识到错误处理方式的演进过程中,每一个新方案的提出都是为了解决上一个方案在使用中产生的坑,并且自身也不可避免的产生了一些新问题,所以go语言目前的错误处理哲学和方案也终将有一天会被更好的思想给替代。
  • 本文主要围绕两个头疼的问题展开,过程中简单对比下try-catch-exception机制,来分析go对于错误处理的新思路以及列出一些解决问题的办法。

go的错误概念

  • 很多语言中采用了exception和error两个概念和一套try-catch-exception机制。
  • 我不打算深究这套机制,只讨论go的概念。
  • 在go中,没有exception,只有error。
  • error“往外抛”的姿势一般是return。这种错误被认为是可恢复的错误。
  • 通过panic方法往外抛的错误,被认为是不可恢复的错误。panic方法接收任意类型,也可以是erorr,它的具体内容在后面讲。
  • 总结:
+ go将错误分为可恢复的错误和不可恢复的错误,它们的区分方式是“往外抛”的姿势。

error类型

  • Go的Error就是一个很普通的interface。

Error接口定义.png

  • error的实现体也是很普通的值。最常用的err := errors.New("string")就是:

errors.New.png

  • errorString设计有一个很巧妙的地方:New方法返回的是一个指针而不是直接返回errorString。
    • 每一次errors.New方法创建的error是不相等的,即使内部包含的s是一样的,也过不了判等操作。
    • 这个特性对于保证错误处理的一方不混乱是至关重要的。
    • 开发中”我遇到一个很像你的人,但始终不是你“这种需求就可以考虑使用指针来实现。

panic

1
2
3
go复制代码func main() {
panic("main panic")
}
  • go中一旦使用的panic,就是想要程序终止。
  • 虽然go有recover的机制来恢复panic,但是在写panic时,不能假设调用者会使用recover。这和try-catch-exception机制中,抛出的exception可认为调用者有义务try-catch的思想是不同的。

使用panic的建议

  • 在项目启动时,资源初始化如加载配置文件、数据库连接等这种一旦失败了,整个项目没有进行下去的意义时,会使用panic。但是也有一些被弱依赖的资源初始化,可能会有一些不同,这要根据项目的实际情况来定。
  • 在写一般函数方法时,如果不是类似索引越界,栈内存溢出等无法恢复的错误,尽量不去使用panic。

recover

  • 一般来说,出现了panic就不需要再救了。
  • 但在类似web服务的框架中,不能由于某一个请求中出现了panic而导致整个服务gg。所以在框架层面会在每一个请求调用链的最上游统一做recover。

recover基本用法

  • recover与defer一起使用
+ 
1
2
3
4
5
6
7
8
go复制代码func main() {
defer func() {
// recover()返回的就是panic()中的入参,不一定时error类型
if err := recover();err != nil {
// 可以根据不同err做不同的处理,如果处理不了这里任可以继续panic
}
}()
}

recover不了的场景

  • recover能处理到情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码func DoSomething() {
defer func() {
if err := recover();err != nil {
// handle
log.Printf("panic被recover了 %v",err)
}
}()
// 在函数内部出现的panic能被recover
// panic(errors.New("panic in DoSomething"))

// 发生在调用函数中出现的panic也能被recover,因为还在一个goroutine中。
// do()
}

func do() {
panic(errors.New("panic in do"))
}
  • recover不能处理的情况
1
2
3
4
5
6
7
8
9
10
go复制代码func DoSomething() {
defer func() {
if err := recover();err != nil {
// handle
log.Printf("panic被recover了 %v",err)
}
}()
// 启动的goroutine中如果出现了panic,DoSomething的recover是没有办法奏效的。
go do()
}
  • 如何处理
+ recover不到的情况其实就是recover的goroutine和panic发生的goroutine不是同一个goroutine。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码// 把需要异步启动的方法包装一下,在异步函数内recover
func Go(f func()) {
go func() {
defer func() {
if err := recover();err != nil {
// handle panic
}
}()
// 实际的异步函数执行
f()
}()
}

// DoSomething开启goroutine的方式变成:
func DoSomething() {
defer func() {
if err := recover();err != nil {
// handle
log.Printf("panic被recover了 %v",err)
}
}()
// 启动的goroutine中如果出现了panic,DoSomething的recover是没有办法奏效的。
Go(do())
}
+ 把goroutine的启动包装一下后,goroutine内的panic就能被recover。但在业务代码中,往往被goroutine包裹的异步函数不会像例子中那样没有入参和返参。上面提供的包装方法中f函数内部的逻辑如果想要使用其外部的变量,只能通过闭包。这里就需要特别注意,开启goroutine时我们认为的外部变量的值和goroutine实际运行时的值是可能不同的。为了减少心智负担,可以在需要的情况下重新定义包装函数(修改包装函数的入参函数的签名),或者确保外部变量只读的。
  • 总结
+ 不建议写这种野生goroutine,它们很难被集中控制,也可能是根本不受控制。建议当需要开启异步任务时,使用channel发送一个信号,由专门的程序监听channel并决定如何开启异步任务,这样可以做到统一的管理。

if err != nil

  • 很多人觉得 if err != nil 很烦,可能由于我自己习惯了,所以觉得还好。在本节就说明下它的好处。

以太坊大佬眼中的error philosophy

if err != nil

  • 我们写的go函数一般在返回一个或者多个正常值的最后会带上一个error类型的值。
  • 当这些正常值要被使用时,必须对error做检查,否则无法保证这些正常值的正确性。

go的错误处理思想

if err != nil的好处

  • No hidden control-flows
+ 清晰的控制流
+ 这是相对于try-catch-exception机制的


    - 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码// try中如果出现了异常,会走到catch分支中。但从代码层面是看不到究竟是哪一个函数遇上了异常。
try
function1()
function2()
function3()
catch 异常
// do something

// go中的做法
err := function1()
if err != nil {
// 处理criticalOperation1()产生的error
}

// ......
- 当然这种对比有失公允,因为只要对每一函数做try-catch效果就和if err != nil 一个效果了,但是if err != nil 经常被吐槽冗余代码多吗,这种情况下try-catch-exception更冗余。
  • No unexpected uncaught exception logs blowing up your terminal (aside from actual program crashes via panics)
+ 不会出现未被捕获的错误使程序崩溃。
+ 还是相对于try-catch-exception机制


    - 
1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码// 下面有多个catch分支去捕获不同的异常,但是如果出现了一个不在catch分支中的异常程序就会崩掉
// 由于在try-catch-exception机制中,抛异常是安全的,所以抛的这一方是很有可能抛出一个catch方没有捕获的异常
// 而go中,error不会导致崩掉,而panic又有严格的规定。
try
function1()
function2()
function3()
catch io异常
// do something
catch 空指针异常
// do something
catch 数据库异常
// do something
  • full-control of errors in your code as values you can handle, return, and do anything you want with
+ 这点显而易见,检查到了error,在分支内可以做处理。

Plan for failure,not success

  • 我认为这句话是整个if err != nil之所以存在的关键所在。这鼓励我们在每一个有可能产生错误的地方都先去检验,然后处理和决定要不要继续往下走。

减少if err != nil的技巧

  • 关于if err != nil的吐槽声一直没有停过,我的认为是,这是它实现上述思想所造成难以避免的问题。但是也能通过一些编程技巧来规避一些重复的error检查。比如Rob Pike(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
38
39
40
41
42
43
44
45
go复制代码_, err = fd.Write(p0[a:b])
if err != nil {
return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
return err
}
// ......

// Write方法绑定在fd上,它会返回一个err。那么这个fd执行多少次Writer就要多少次 if err != nil 操作
// 只要做一个改造就能减少多余的 if err != nil

// 定义一个结构体包裹了fd(io.Writer)和err
type errWriter struct {
w io.Writer
err error
}

// errWriter的write方法相当于包装了之前的fd.Write方法,不过要在真正执行前,检验下其内部的err是否为空
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}

func (ew *errWriter) Err() error {
return ew.err
}

// 然后代码就变成了
b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// ...
// 统一返回error
if b.Err() != nil {
return b.Err()
}
+ 标准库bufio.Scanner也做了类似的事情



1
2
3
4
5
6
7
8
9
10
11
go复制代码scanner := bufio.NewScanner(input)

// Scan函数的工作是”翻页“,普通的逻辑应该是返回“翻到哪了”, 而标准库的设计是返回“翻成功了没”,即bool类型
// 至于翻到哪了,翻错的原因是什么都包装在scanner内部,按需取就行。这种设计就会节省很多if err != nil
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
+ 有一些场景是很适合这种建议的: - 数据库操作一般都是先select然后多个join然后多个where,还有可能加个排序,分页之类的操作,如果每一步都做if err != nil 代码会非常繁琐,gorm的解决方法就类似。 + Rob Pike大佬的建议引发我的一个思考: - 这种设计是不是很像try-catch-exception?只是在上面很多个b.Write()调用中的某一个调用出现错误时,try-catch-exception机制会直接跳到catch分支,而这种机制会继续尝试执行之后的代码,但会在最开始的错误检查被挡,最后落到类似于catch分支的if b.Err() != nil。 - go的错误处理理念中`Plan for failure,not success`这里没做到,`No hidden control-flows`这里也没做到。有点穿新鞋走老路的意思。 - 这让我有一种思考:设计哲学,语言之禅这类的规范,在实际的开发,是不可能做到完全遵守的,相反它们只是一个指导思想,并不是需要强制执行的“法律”。

定义error的几种姿势

  • 参考Dave cheney GoCon 2016 的演讲关于error处理方式的部分

Sentinel error

  • 称为哨兵error,在代码中预定义好的错误类型。
  • 如标准库io包中:

哨兵error.png

  • 抛错误的一方直接return EOF这类定义好的错误
  • 错误处理方一般使用判等的方式如 if err == io.EoF来判断错误原因。

弊端

  • 由于哨兵error的使用者一般都是用判等的方式,那就无法为error添加上下文。比如A函数调用io包中提供的函数时,捕获到io.EOF的错误。在A中如果使用fmt.Errorf方法为其加上错误发生的上下文,由于fmt.Errorf底层使用了errors.New(text string)新建了一个error,则调用A函数的B函数则无法使用if err == io.EoF。在项目中往往会演进成在B函数中使用io.Error方法获取字符串后,再进行字符串匹配。
+ 字符串匹配本身就有风险,很有可能两个错误内部的string恰巧对上了。
+ Dave cheney在文章中也说了,error.Error方法是给人看的,而不是程序。它只应该出现在日志文件、终端打印等这些位置。但是他也说明了是不可能完全做到这一点的,不过要有尽可能避免的意识。
  • 哨兵一旦被定义,就会被使用,一旦被使用,就很难更改。
+ 如果我们使用了哨兵error,在定义接口的时候,就会在接口文档中说明在某种情况下会返回某种哨兵error。那么在接口的所有实现中就必须使用这种error,即使之后定义了更加细致的哨兵error,也不能推广了,因为调用者已经用上了。
  • 导致代码耦合。
+ 一个项目有很多模块,每一个模块中定义了自己的一些哨兵error,那使用者不得不引入这些包,包多了,要么同一意义的错误重复定义,要么极易导致循环引入。
+ 虽然很多基础库使用了哨兵error,但是我们自己程序最好不要模仿。一来我们的业务场景往往不像某一个具体的标准库那样职责单一而且相互之间的关联没那么多,二来我们也不具备标准库作者们那样强大的抽象能力和时间精力去投入一个快速迭代的业务模块。

⚠️:业务错误码也是一种哨兵error,但是业务错误码是系统作为一个整体向外暴露的,不存在耦合问题,是具有很大价值的。

Error type

  • 自定义一种error的实现type也是广泛运用的方式。

error type.png

  • error type实现了error接口,调用需要对函数返回的err做类型断言,而不再是判等。
  • 这种形式的error处理最大的好处就是error type可以包裹原本的error的同时可以附带很多上下文信息,并且还能定义其他方法,具备一定的灵活性。

弊端

  • 它仍然没有解决sentinel error增加api表面积的问题。

Opaque errors

什么是Opaque errors

  • 不透明的error。
  • 在大多情况,在A函数的调用者发现A函数出现了错误的时候,他们能做的都是将error继续return出去,在写代码的时候并不关心error里面有什么。比如os.open(name string)打开一个文件,调用者传入了一个错误的name,以至于open失败了,这个时候的error,我们在代码层面并不需要探到error的原因,然后更改name,然后重试。相反需要做的是直接return,通知调用链的上游这里出现了error。在整条调用链的最上游打印出日志,让开发者或者使用者知道原因(这里也呼应了sentinel error论述中Dave cheney提到的 error.Error方法是给人看的,不是给程序看的思想 )。这样error就是Opaque error。
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码func main() {
file,err := OpenFileAndDoSomething("/myfile/go语言从入门到精通.pdf")
if err != nil {
// 调用链的终端打印出来,从日志很清楚就能看得出引发错误的原因。
log.Print(err)
return
}
}

func OpenFileAndDoSomething(name string) error {
file,err := os.open(name)
if err != nil {
// 在代码的层面不需要探究error的原因
// 可以在这里记录一些上下文(使用fmt.Errorf())
// 出现错误不要继续往下执行了
return err
}
// ...... 后续操作
}

Assert errors for behaviour, not type

  • 当然,依然存在着我们必须依据error的内容做出处理的情况。比如网络请求时,返回token过期的错误,这种错误是可以通过重新获取token然后发起重试解决的。但它需要探究error原因。Opaque errors做不到。
  • 这就需要断言,如本节的标题说的那样,好的断言方式是断言行为,而不是断言类型。
  • Sentinel error 和 Error type都是我们自定义的error,然后向包外暴露的。上面提到过一旦向外暴露的东西就会被使用,一旦被使用,就很难改。而对于错误定义这种是很难的在最开始的时候就定义精准的,难免需要更改。
  • 如果将向外暴露类型,让调用者对error做类型断言改成向外暴露一种行为断言方法。
  • 举个k8sapi项目的例子说明:

k8s api 错误行为.png

  • 上面这一系列的函数从签名就能看出,主要功能是对传入的error做一个行为的判断,比如func IsTimeOut(err error) bool就是判断err是否是一个超时的错误。
+ 用简单代码说明下用法:


    - 假设调用方,需要对某种特定类型的error做特殊的处理,比如是ownError
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码// 对外提供的方法,用于判断类型
func IsOwnError(err error) ok {
// ...类型断言
}

// 特定类型的错误,实现了error接口。
// 甚至可以把ownError类型抽象成接口,这样会更加灵活
type ownError struct {}

func (e *ownError) Error() string {
return "is ownError"
}

func Exec() error {
return &ownError{}
}
  • 这样的好处
+ 包内无需向外暴露具体的错误类型,只需要暴露一个方法就可以做到错误原因的判断,很大程度上减少了向外暴露的面积。
+ 如在`func IsTimeOut(err error) bool`中。由于外部的错误处理时没有和某一个具体的错误类型绑定上关系(Sentinel error的判等 和 Error type的类型断言都绑的死死的),满足TimeOut的错误类型不是固定的,那我们可以很灵活的定义具体的错误类型。
  • 很多库都是使用的类似方法,gorm也是,都可以参考。

处理error — Only handle errors once

  • 处理错误有一个总的理念:Only handle errors once,错误只处理一次。
  • 理解这句话就是两个点,处理和一次。
  • 假设我们有个功能要查询用户信息,包括昵称,头像,个性签名等信息,其中头像的url单独存在一个数据库中。当高峰请求的时候,有可能头像数据库崩了,那在查询头像url的这一步操作肯定会报一个error。那对于这一error的处理会有很多种,下面举出来一些:
+ 1.服务降级,当这个error发生时,就当没有这回事情,吞掉error,继续之后的执行。


    - 
1
2
3
4
5
6
7
8
go复制代码func GetUserInfo(id string) (UserInfo,error) {
url,err := iconDao.GetIconUrl(id)
if err != nil {
log.Infof("iconDao.GetIconUrl(id) error:%+v",err)
err = nil // 直接吞掉 error
}
// ...... 查询其他信息后组装
}
+ 2.不能接受服务降级,直接抛错。 -
1
2
3
4
5
6
7
8
9
10
11
go复制代码func GetUserInfo(id string) (UserInfo,error) {
url,err := iconDao.GetIconUrl(id)
if err != nil {
// return终止后续操作
// iconDao.GetIconUrl(id)的错误直接返回可能外层不能直观看到错误出现的上下文信息
// 这里可以带上一些上下文 err = fmt.Errof("GetUserInfo id = %s iconDao.GetIconUrl failed:%v",err)
// fmt.Errorf()底层会创建一个新的error,但如果这里err使用的是哨兵error,那么上层的判断操作会被打破。
return nil,err
}
// ...... 查询其他信息后组装
}
+ 3.重试一下,再次失败再抛出。 -
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func GetUserInfo(id string) (UserInfo,error) {
url,err := iconDao.GetIconUrl(id)
if err != nil {
// 再试一次
url,err = iconDao.GetIconUrl(id)
if err != nil {
// 出错抛出
return nil,fmt.Errof("GetUserInfo id = %s iconDao.GetIconUrl failed:%v",err)
}
}
// ...... 查询其他信息后组装
}
  • 在上面举的3个例子,处理error的结果有两种,一种是err变为nil了,可能是不用管,也可能是重试之后解决了,另一种是return出去了,要么是直接return的要是重试之后仍然还有error。不管怎么样,都不能对被调用函数返回的error无动于衷一定得处理。
  • 另外处理只需要一次,return是一种处理方式,不管error是一种处理方式,重试也是一种处理方式。上面例子中是没有重复处理的。
  • 我们经常犯的重复处理的错误就是log+return。这种方式导致的后果就是一个error的信息打印了很多次,看日志排查时非常麻烦。事实上,log就是一种处理方式,为什么满屏冗余的error日志,因为对于同一个error,处理了两次。违背了Only handle errors once原则。比如:
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码func GetUserInfo(id string) (UserInfo,error) {
url,err := GetIconUrl(id)
if err != nil {
log.Printf("查询用户头像失败 error:%v",err)
return nil,err
}
// ...... 查询其他信息后组装
}

func GetIconUrl(id string) (string,error) {
// 权限校验
ok,err :=verifyAuth(id)
// 这里即打印了日志,又返回了err。对于err处理了两次
if err != nil {
log.Printf("权限校验失败 error:%v",err)
return "",err
}
// iconDao.GetIconUrl(id)的返回值和此函数的返回值是match的,直接return就又少了一次if err != nil 生活小妙招
return iconDao.GetIconUrl(id)
}

// GetIconUrl中处理了两次error,其中包括一次打印。调用它的GetUserInfo又处理了两次,也包括一次打印。而GetUserInfo的上游调用又有可能处理两次,日志重复打印,形成日志噪音,不仅很难找需要的日志,还有可能让我们不小心忽视掉有用的错误。
  • 总结:错误处理的核心是:一个错误只处理一次,遇上错误不能恢复(如重试等)也不能吞掉(如服务降级)的时候,最好的方式就是创建新的error,包含此处的上下文和旧error的错误信息后向上抛出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码func GetUserInfo(id string) (UserInfo,error) {
url,err := GetIconUrl(id)
if err != nil {
err = fmt.Errorf("GetUserInfo 权限GetIconUrl校验失败 error:%s",err.Error())
return nil,err
}
// ...... 查询其他信息后组装
}

func GetIconUrl(id string) (string,error) {
ok,err :=verifyAuth(id)
if err != nil {
err = fmt.Errorf("GetIconUrl 权限校验失败 error:%s",err.Error())
return nil,err
}
return iconDao.GetIconUrl(id)
}
+ 打印日志的活交给调用链的最顶端集中处理。最终的打印出来的error是:
+ `GetUserInfo 权限GetIconUrl校验失败 error:GetIconUrl 权限校验失败 error:权限不足`
+ 其实这里error内部也包含了一些重复没必要的东西,后面讲wrap的时候会提到解决方法。
+ 但是go的error不像其他语言的exception,go创建新的error后,会丢失旧error的堆栈信息,不便于排查是找到出问题的那一行代码,倒是直接panic会保存住堆栈信息。很显然不可能因为需要堆栈的缘故用panic取代替error。[github.com/pkg/errors](https://github.com/pkg/errors)解决了这一问题。

Wrap errors

  • github.com/pkg/errors包让我们在创建新error的时候,不是直接放弃旧error。而是通过wrap的方式,新的error包装旧的,层层包装,哪一个error也不会被丢掉。

怎么用

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
go复制代码package main

import (
"fmt"
"log"
"os"

"github.com/pkg/errors"
)

// 这里存在一个重复wrap的问题,后面会说明如何避免。

// 这里只会简单的说明几个关键方法的用法,后文会分析它们的代码。

// 整个调用链是 main -> func3 -> func2 -> func1 main是调用链源头
// func1是出现根错误的地方,func2调用func1出现错误后秉承只处理一次的原则,直接return。func3类似操作。
// main作为调用链的源头,处理error的方式选择的是打印日志。
func main() {
err := func3()
if err != nil {
// Cause可以找到被Wrap的根err
log.Printf("original error:%T %v\n",errors.Cause(err),errors.Cause(err))
// 打印Wrap过的err 可以通过%+v获取到调用的堆栈信息
log.Printf("stack trace:\n%+v\n",err)
os.Exit(1)
}
}

func func3() error {
err := func2()
if err != nil {
// Wrap error可以携带上下文
return errors.Wrap(err,"fun3 call func2 error")
}
fmt.Println("i am func3")
return nil
}

func func2() error {
err := func1()
if err != nil {
// Wrap error可以携带上下文
return errors.Wrap(err,"func2 call func1 error")
}
fmt.Println("i am func2")
return nil
}

func func1() error {
// 产生根error
return errors.New("fun1 root error")
}

Wrap方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// copy出Wrap的源码
func Wrap(err error, message string) error {
if err == nil {
return nil
}
// withMessage是附带上下文但是不丢失根error的原因
// 不像fmt.Errorf那样取出根error内部的string,组合上下文创建一个新error。
err = &withMessage{
cause: err,
msg: message,
}
// withStack是保住堆栈信息的原因
// err是withMessage,callers是堆栈信息
return &withStack{
err,
callers(),
}
}

Cause方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// copy出Cause的源码
// Cause和Wrap方法是相反的,它的目的是找被包住的根error
func Cause(err error) error {
type causer interface {
Cause() error
}

// 一直往里面解开wrap,找到根error
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}

Wrap的使用注意

  • 在上面的例子中,如果在运行了就会发现,打印的堆栈信息特别多,而且重复了。这是因为对同一个error重复Wrap了。如果打印重复的堆栈,比起重复打印error本身造成的日志噪音问题更麻烦,毕竟堆栈信息一般都是很多的。
  • 这里举出几种使用注意事项,未来在代码中发现了新的坑也可以加上来。
    • 在开发应用代码时(比如一个web项目,而非是基础/工具库),使用errors.New或errors.Errorf返回一个新的错误。注意这里的errors不是标准库的errors,而是github.com/pkg/errors,它创建error时已经带上了堆栈信息了。
      • 如果是开发基础/工具库的时候,使用普通的error就好了。因为调用者会对error做Wrap的。
    • 如果调用同一包内的函数,简单返回error,不需要wrap。
      • 因为包内的函数在它的内部或者它调用链的下游发现根因的时候已经Wrap过了。
        • 这个思想就解决了之前处理error — Only handle errors once中总结部分出现的问题。那里对应包内函数返回的错误也加上了上下文信息,其实就重复了。
    • 和其他库协作 wrap保存根因
      • 当引入标准库或者github上的一些基础库时,如gorm。其他库返回的错误需要Wrap起来,并且带上足够的上下文信息。
  • 在程序的顶部或者是工作的goroutine顶部,使用%+v把堆栈详情记录
    • 示列代码中有过示范。

总结

  • 一个错误在他的整个生命流程中只需要处理一次,而不是发现一次处理一次。特别注意打印了错误日志也算处理过了。
  • error一旦被处理过了,就不算error了而是nil。
  • 当发现error又不打算处理时,使用Wrap带上足够的上下文往上抛即可。
  • 避免重复的Wrap。

Go1.13

  • 先看一个背景
+ 
1
2
3
4
5
6
7
8
9
go复制代码// 当我们对Sentinel error做等值判断时
if err == io.EOF {
// ***
}

// 当对Error type做类型断言时
if e,ok := err.(*os.PathError);ok {
// ***
}
  • github.com/pkg/errors的理念被Go官方采用了,于是在Go1.13后的版本,在fmt和errors做了改造。
+ fmt包出现了`%w占位符`:


    - 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码if err != nil {
return fmt.Errorf("需要附带的上下文信息 %w",err)
}

// 看下 %w背后做的事情 copy出fmt中源码
// 这看上去是不是和github.com/pkg/errors中的Wrap函数做的事情似成相识?
// %v的方式丢弃了原error除了文本信息之外的所有信息,而%w会将旧错误包装起来
type wrapError struct {
msg string
err error
}

func (e *wrapError) Error() string {
return e.msg
}

func (e *wrapError) Unwrap() error {
return e.err
}
+ errors包中出现了Is方法 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码// 在背景中提到过对Sentinel error做等值判断,看起来没什么不好
// 但是如果采用了%w对error做包装,等值判断就是失效了。如果使用的是%v,那就完蛋了,不可能再做判等了。
// 好在%w只是包装了,没有丢弃,而且errors包提供了UnWrap方法拿到内部的error

// errors.UnWrap源码 能看出其实和github.com/pkg/errors的Cause方法差不多
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}

// 如果调用UnWrap去拿根error,由于error可能被重复%w,所以这里会很麻烦。
// errors提供了Is(err,target error)方法。
// target就是指Sentinel error,如io.EOF。只要err中任意一层包含了target,就会返回true。不需要我们手动UnWrap了。

// 并且Is方法还有一个功能
// 我们在自定义Sentinel error时,可以为其自定义判等方法,而不单单是用==。
// 只要Sentinel error实现如下接口,errors提供的Is(err,target error)方法就会使用自定义的判等方法
// 接口 :interface{ Is(error) bool }
+ errors包中还出现了As方法 -
1
2
3
4
5
6
7
8
9
go复制代码// 和Is方法想解决的问题一样,As方法主要用于解决Error type的问题
// Error type需要对错误做类型断言
// errors提供了As(err error, target interface{})方法。
// target传如需要被断言的类型的指针。只要err中任意一层能断言成target,就会返回true。
// 用法:
var e *os.PathError
if errors.As(err,&e) {
// 如果断言成功 e就可以直接使用
}
  • 这样看来,似乎可以完全放弃github.com/pkg/errors了,但是%w没有保存堆栈信息(实现不知官方出于何种原因,都Wrap了还不带上堆栈)。
  • 好在github.com/pkg/errors对Go1.13做了兼容,就可以使用github.com/pkg/errors方法保存堆栈,用标准库errors的Is/As方法处理处理错误。
+ 
1
2
3
4
5
6
go复制代码// copy github.com/pkg/errors代码

// Unwrap provides compatibility for Go 1.13 error chains.
func (w *withStack) Unwrap() error { return w.error }

// 上述Is/As方法内部都是调用UnWrap方法,所以github.com/pkg/errors中的withStack实现了接口,那使用github.com/pkg/errors的Wrap方法替代标准库的%w就可以即留住堆栈又能使用Is/As方法。

总结

  • 错误处理是一个很大的话题,更是一个需要不断改进的话题。可以说没有哪一种语言或者哪一种思想就一定比其他的都要好,得到一些便利的时候往往会造成另一些不想遇到的问题。所以关于错误处理的话题讨论,远远没有结束!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用命令模式重构播放器控制条

发表于 2021-11-22

本文节选自《设计模式就该这样学》

1 命令模式的UML类图

命令模式的UML类图如下图所示。

file

2 使用命令模式重构播放器控制条

假如我们开发一个播放器,播放器有播放功能、拖动进度条功能、停止播放功能、暂停功能,我们在操作播放器的时候并不是直接调用播放器的方法,而是通过一个控制条去传达指令给播放器内核,具体传达什么指令,会被封装为一个个按钮。那么每个按钮就相当于对一条命令的封装。用控制条实现了用户发送指令与播放器内核接收指令的解耦。下面来看代码,首先创建播放器内核GPlayer类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
public class GPlayer {

public void play(){
System.out.println("正常播放");
}

public void speed(){
System.out.println("拖动进度条");
}

public void stop(){
System.out.println("停止播放");
}

public void pause(){
System.out.println("暂停播放");
}

}

创建命令接口IAction类。

1
2
3
4
5
6
java复制代码
public interface IAction {

void execute();

}

然后分别创建操作播放器可以接收的指令,播放指令PlayAction类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
public class PlayAction implements IAction {

private GPlayer gplayer;

public PlayAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.play();
}

}

暂停指令PauseAction类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
public class PauseAction implements IAction {

private GPlayer gplayer;

public PauseAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.pause();
}

}

拖动进度条指令SpeedAction类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
public class SpeedAction implements IAction {

private GPlayer gplayer;

public SpeedAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.speed();
}

}

停止播放指令StopAction类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码
public class StopAction implements IAction {

private GPlayer gplayer;

public StopAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.stop();
}
}

最后创建控制条Controller类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
public class Controller {
private List<IAction> actions = new ArrayList<IAction>();
public void addAction(IAction action){
actions.add(action);
}

public void execute(IAction action){
action.execute();
}

public void executes(){
for(IAction action : actions){
action.execute();
}
actions.clear();
}
}

从上面代码来看,控制条可以执行单条命令,也可以批量执行多条命令。下面来看客户端测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码
public static void main(String[] args) {

GPlayer player = new GPlayer();
Controller controller = new Controller();
controller.execute(new PlayAction(player));

controller.addAction(new PauseAction(player));
controller.addAction(new PlayAction(player));
controller.addAction(new StopAction(player));
controller.addAction(new SpeedAction(player));
controller.executes();
}

由于控制条已经与播放器内核解耦了,以后如果想扩展新命令,只需增加命令即可,控制条的结构无须改动。

3 命令模式在JDK源码中的应用

首先来看JDK中的Runnable接口,Runnable相当于命令的抽象,只要是实现了Runnable接口的类都被认为是一个线程。

1
2
3
4
java复制代码
public interface Runnable {
public abstract void run();
}

实际上调用线程的start()方法之后,就有资格去抢CPU资源,而不需要编写获得CPU资源的逻辑。而线程抢到CPU资源后,就会执行run()方法中的内容,用Runnable接口把用户请求和CPU执行进行解耦。

4 命令模式在JUnit源码中的应用

再来看一个大家非常熟悉的junit.framework.Test接口。

1
2
3
4
5
6
7
8
java复制代码
package junit.framework;

public interface Test {
public abstract int countTestCases();

public abstract void run(TestResult result);
}

Test接口中有两个方法,第一个是countTestCases()方法,用来统计当前需要执行的测试用例总数。第二个是run()方法,用来执行具体的测试逻辑,其参数TestResult是用来返回测试结果的。实际上,我们在平时编写测试用例的时候,只需要实现Test接口就被认为是一个测试用例,那么在执行的时候就会被自动识别。通常做法都是继承TestCase类,不妨来看一下TestCase的源码。

1
2
3
4
5
6
7
8
java复制代码
public abstract class TestCase extends Assert implements Test {
...
public void run(TestResult result) {
result.run(this);
}
...
}

实际上,TestCase类也实现了Test接口。我们继承TestCase类,相当于也实现了Test接口,自然就会被扫描成为一个测试用例。
关注『 Tom弹架构 』回复“设计模式”可获取完整源码。

【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注『 Tom弹架构 』可获取更多技术干货!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文搞懂Java的SPI机制 1 简介 源码 使用 适用场景

发表于 2021-11-22

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」

1 简介

SPI,Service Provider Interface,一种服务发现机制。

有了SPI,即可实现服务接口与服务实现的解耦:

  • 服务提供者(如 springboot starter)提供出 SPI 接口。身为服务提供者,在你无法形成绝对规范强制时,适度”放权” 比较明智,适当让客户端去自定义实现
  • 客户端(普通的 springboot 项目)即可通过本地注册的形式,将实现类注册到服务端,轻松实现可插拔

缺点

  • 不能按需加载。虽然 ServiceLoader 做了延迟加载,但是只能通过遍历的方式全部获取。如果其中某些实现类很耗时,而且你也不需要加载它,那么就形成了资源浪费
  • 获取某个实现类的方式不够灵活,只能通过迭代器的形式获取

Dubbo SPI 实现方式对以上两点进行了业务优化。

源码


应用程序通过迭代器接口获取对象实例,这里首先会判断 providers 对象中是否有实例对象:

  • 有实例,那么就返回
  • 没有,执行类的装载步骤,具体类装载实现如下:

LazyIterator#hasNextService 读取 META-INF/services 下的配置文件,获得所有能被实例化的类的名称,并完成 SPI 配置文件的解析

LazyIterator#nextService 负责实例化 hasNextService() 读到的实现类,并将实例化后的对象存放到 providers 集合中缓存

使用

如某接口有3个实现类,那系统运行时,该接口到底选择哪个实现类呢?
这时就需要SPI,根据指定或默认配置,找到对应实现类,加载进来,然后使用该实现类实例。

如下系统运行时,加载配置,用实现A2实例化一个对象来提供服务:

再如,你要通过jar包给某个接口提供实现,就在自己jar包的META-INF/services/目录下放一个接口同名文件,指定接口的实现是自己这个jar包里的某类即可:

别人用这个接口,然后用你的jar包,就会在运行时通过你的jar包指定文件找到这个接口该用哪个实现类。这是JDK内置提供的功能。

我就不定义在 META-INF/services 下面行不行?就想定义在别的地方可以吗?

No!JDK 已经规定好配置路径,你若随便定义,类加载器可就不知道去哪里加载了

假设你有个工程P,有个接口A,A在P无实现类,系统运行时怎么给A选实现类呢?
可以自己搞个jar包,META-INF/services/,放上一个文件,文件名即接口名,接口A的实现类=com.javaedge.service.实现类A2。
让P来依赖你的jar包,等系统运行时,P跑起来了,对于接口A,就会扫描依赖的jar包,看看有没有META-INF/services文件夹:

  • 有,再看看有无名为接口A的文件:
    • 有,在里面查找指定的接口A的实现是你的jar包里的哪个类即可

适用场景

插件扩展

比如你开发了一个开源框架,若你想让别人自己写个插件,安排到你的开源框架里中,扩展功能时。

如JDBC。Java定义了一套JDBC的接口,但并未提供具体实现类,而是在不同云厂商提供的数据库实现包。

但项目运行时,要使用JDBC接口的哪些实现类呢?

一般要根据自己使用的数据库驱动jar包,比如我们最常用的MySQL,其mysql-jdbc-connector.jar 里面就有:

系统运行时碰到你使用JDBC的接口,就会在底层使用你引入的那个jar中提供的实现类。

案例

如sharding-jdbc 数据加密模块,本身支持 AES 和 MD5 两种加密方式。但若客户端不想用内置的两种加密,偏偏想用 RSA 算法呢?难道每加一种算法,sharding-jdbc 就要发个版本?

sharding-jdbc 可不会这么蠢,首先提供出 EncryptAlgorithm 加密算法接口,并引入 SPI 机制,做到服务接口与服务实现分离的效果。
客户端想要使用自定义加密算法,只需在客户端项目 META-INF/services 的路径下定义接口的全限定名称文件,并在文件内写上加密实现类的全限定名


这就显示了SPI的优点:

  • 客户端(自己的项目)提供了服务端(sharding-jdbc)的接口自定义实现,但是与服务端状态分离,只有在客户端提供了自定义接口实现时才会加载,其它并没有关联;客户端的新增或删除实现类不会影响服务端
  • 如果客户端不想要 RSA 算法,又想要使用内置的 AES 算法,那么可以随时删掉实现类,可扩展性强,插件化架构

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…235236237…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%