写完了代码第一件事是编译,编译失败只能根据编译器通知修改代码;编译通过了第二件事是跑UT,UT不过需要进行调试。调试包括debug和release包的调试,线上进程的运行问题有时候也需要调试,调试的主要方式是调试工具和日志(包括print大法)。为了发现问题,有时候还需要添加报警日志。

提高代码的健壮性,编译器、静态检查和格式化工具、调试、单元测试、日志等是开发必不可少的

C语言和C++

编译

为什么需要编译?

  1. 计算方面,汇编代码基本可以等同于机器码。汇编语言和机器码是一行一行指令执行,高级语言的会抽象出来if-else条件执行/while循环执行和函数执行模块,编译需要把这些模块转化成一行一行的指令;不同cpu提供的指令和寄存器不同,因而编译器的目标指令也不同(其他硬件需要和cpu兼容,一般程序只需要操纵cpu就可以同时操纵内存、磁盘、网卡等硬件);另外,由操作系统实现中断、上下文切换等计算单元(不需要用户程序实现),也需要编译器把它们打包到编译后的二进制文件中。
  2. 数据方面,汇编语言没有类型概念,需要手动指定寄存器和内存物理地址传输数据,数据交换一般是cpu字长对齐的(64位cpu8字节对齐);高级语言使用类型来确定数据内存分配大小,使用变量维护某一块内存区域,需要编译器记录类型变量的地址,使用变量转化为从某寄存器或内存地址拿数据的指令,动态内存转化为执行从空闲内存拿内存的指令。

编译把.cpp 转化为 .o文件。对于多.cpp文件或使用到动态/静态库的文件(几乎所有.cpp都会使用到库文件,例如glibc),需要使用链接把多个.o文件链接成一个二进制文件。链接期间还会链接跨文件共享的外部变量/全局变量。

C++ 20引入了模块,模块相当于融合了头文件和源文件(库),编译、依赖和隔离机制更加清晰,但广泛使用还需要一定距离。

C语言和C++一般公用编译器,常用的编译器有两个,gcc和clang/llvm。常见的gcc编译指令

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
g++ -c file.cpp  # 只编译
g++ file.cpp -o program # 编译链接
g++ file1.cpp file2.cpp -o program # 多文件编译

-O0 # 不进行优化(默认)。
-O1 # 基础优化,平衡编译速度和运行效率。
-O2 # 更高的优化级别,提高性能。
-O3 # 最高优化级别,可能增加编译时间和可执行文件体积。
-Os # 优化以减小可执行文件的大小。

-g # 生成调试信息,用于调试器(如 gdb)。
-ggdb # 为 GNU 调试器生成更详细的调试信息。

-std=c++11 # 使用 C++11 标准。
-std=c++14 # 使用 C++14 标准。
-std=c++17 # 使用 C++17 标准。
-std=c++20 # 使用 C++20 标准。

# 警告选项
-Wall # 启用大多数常见的警告。
-Wextra # 启用额外的警告。
-Werror # 将警告视为错误。
-pedantic # 强制严格遵循标准。

# 链接选项
-l # 指定链接库。
-L # 指定库文件搜索路径。
-I # 指定头文件搜索路径。
g++ file.cpp -o program -lm -L/usr/lib -I/usr/include

g++ -S file.cpp -o file.s # 生成汇编代码

g++ -DDEBUG file.cpp -o program # 定义宏

# 生成动态库
g++ -fPIC -c file1.cpp file2.cpp # -fPIC 生成位置无关代码
g++ -shared -o libmylib.so file1.o file2.o # -shared 指定生成动态库。
g++ main.cpp -L. -lmylib -o program

makefile是GNU推出的编译工具,GNU Autotools使用 autogen.sh + configure + make经典编译流程

  1. ./autogen.sh 生成 configure 脚本和 Makefile.in 模板。
  2. ./configure [options] 配置编译选项
  3. make 利用makefile编译代码
    这一套流程比较原始,只推荐编译老项目,不建议在新项目使用
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
CC = g++  # 变量定义
CFLAGS = -Wall -g
OBJ = main.o utils.o
TARGET = program

# 规则
$(TARGET): $(OBJ)
$(CC) $(CFLAGS) -o $@ $^

main.o: main.cpp utils.h
$(CC) $(CFLAGS) -c $<

utils.o: utils.cpp utils.h
$(CC) $(CFLAGS) -c $<

# 伪目标
clean:
rm -rf $(OBJ) $(TARGET)

# $@ 当前目标的名字。
# $< 第一个依赖文件。
# $^ 所有依赖文件。

make # 构建目标
make clean # 清理构建文件

现代的编译工具是cmake和bazel。

  1. CMake 是一个脚本,能自动生成 Makefile 或其他构建系统文件(如 Ninja 或 Visual Studio 项目文件)
  2. google提供的bazel 除了是编译工具, 还可以作为包管理工具。bazel 一次编译好代码,不再使用make中间步骤

cmake 构建语法

cmake构建过程一般是经典两步

  1. cmake -DCMAKE_BUILD_TYPE=Debug ..
  2. make

基本命令

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
cmake_minimum_required(VERSION 3.10)  # CMake 最低版本要求

project(MyProject VERSION 1.0 LANGUAGES C CXX) # 定义项目名称和语言

add_executable(myapp main.cpp) # add_executable 添加可执行文件

add_library(mylibrary STATIC lib.cpp) # add_library 添加库

target_link_libraries(myapp mylibrary) # target_link_libraries 将库链接到目标

include_directories(${CMAKE_SOURCE_DIR}/include) # include_directories为编译器添加头文件搜索路径。

# add_dependencies 控制目标之间的构建顺序,构建顺序
# add_dependencies(<target> <depend1> <depend2> ...)

set(MY_VAR "Hello") # set 设置变量值

message(STATUS "This is a status message.") # message 打印信息,支持不同的输出级别
message(WARNING "This is a warning message.")
message(ERROR "This is an error message.")

if(MY_VAR STREQUAL "Hello") # if()判断条件
message(STATUS "Hello")
elseif(MY_VAR STREQUAL "World")
message(STATUS "World")
else()
message(STATUS "Something else")
endif()

find_package(OpenGL REQUIRED) # find_package 查找外部库或软件包

# find_program 搜索指定的可执行程序,并将其路径存储到指定的变量中。HINTS优先查找的路径
set(BUSTUB_CLANG_SEARCH_PATH "/usr/local/bin" "/usr/bin" "/usr/local/opt/llvm/bin")
find_program(CLANG_FORMAT_BIN
NAMES clang-format clang-format-14
HINTS ${BUSTUB_CLANG_SEARCH_PATH})

option(MY_FEATURE "Enable MyFeature" ON) # option 定义布尔选项,通常用于启用或禁用功能

install(TARGETS myapp DESTINATION bin) # install 指定安装规则

# CMake 在父目录中执行 add_subdirectory() 时,CMake 会进入子目录 <source_dir>,并寻找子目录中的 CMakeLists.txt 文件
add_subdirectory()

CMAKE_SOURCE_DIR # 项目源代码的根目录
CMAKE_BINARY_DIR # 构建目录
CMAKE_CURRENT_SOURCE_DIR # 当前 CMake 脚本所在目录。
CMAKE_CURRENT_BINARY_DIR # 当前 CMake 构建目录。
CMAKE_CXX_COMPILER # C++ 编译器。
CMAKE_BUILD_TYPE # 构建类型(如 Debug, Release)

STREQUAL # STREQUAL 用于比较两个字符串是否相等,区分大小写
EXISTS # 逻辑判断命令,用于检查某个文件或目录是否存在

file
# file(<operation> <arguments>...)
# file(READ "<file_path>" <variable_name>) 读取文件内容,保存到变量
# file(TO_CMAKE_PATH) 将输入路径转换为 CMake 使用的标准路径格式

enable_testing() # 启用 CTest 功能,CMake 脚本中使用 add_test() 来定义测试用例,并通过 ctest 命令执行测试。

string()
# string(<COMMAND> <ARGUMENTS>) 字符串处理函数
# string(CONCAT <VAR> <STRING1> <STRING2> ...) 将多个字符串连接成一个字符串,并将结果存储到变量

add_custom_target # 运行命令,例如执行clang-tidy
add_custom_target(format ${BUSTUB_BUILD_SUPPORT_DIR}/run_clang_format.py
${CLANG_FORMAT_BIN}
${BUSTUB_BUILD_SUPPORT_DIR}/clang_format_exclusions.txt
--source_dirs
${BUSTUB_FORMAT_DIRS}
--fix
--quiet
)

gtest_discover_tests # gtest_discover_tests 是 CMake 中与gtest集成的测试自动发现和注册

find_package(GTest REQUIRED)

add_executable(my_test_target test_case1.cpp test_case2.cpp) # 添加 Google Test 测试目标
target_link_libraries(my_test_target GTest::GTest GTest::GMock)

bazel构建和包管理工具

Bazel 的构建过程通常由以下几个步骤组成:

  • BUILD 文件:每个项目都有一个 BUILD 文件,定义了如何构建该项目。通过这个文件,Bazel 知道如何处理源代码、依赖项、编译步骤等。
  • 目标(Target):在 BUILD 文件中,你可以定义“目标”,例如编译一个库、一个可执行文件或一个测试。这些目标描述了项目的不同部分,Bazel 会根据目标来决定如何进行构建。
  • 依赖关系:Bazel 会根据目标的依赖关系,构建出一个有向无环图(DAG),确保构建过程是有序的,并且只构建必要的部分。
  • 增量构建:Bazel 会智能地检测哪些部分发生了变化,避免每次都从头开始构建。它通过文件哈希和时间戳来确定哪些目标需要重新构建。

项目根目录下创建一个 WORKSPACE 文件,Bazel 会将其作为项目的工作空间标识。
src/BUILD

1
2
3
4
5
6
7
cc_binary(
name = "myapp",
srcs = ["main.cc"],
deps = [
":myheader", # 依赖其他目标(例如头文件或库)
],
)

src/main.cc

1
2
3
4
5
6
#include <iostream>

int main() {
std::cout << "Hello, Bazel!" << std::endl;
return 0;
}

直接执行bazel build //src:myapp 编译

  1. //src:myapp:表示从 src 目录中的 BUILD 文件中构建目标 myapp
  2. bazel build:该命令会自动解析 BUILD 文件,并根据其中的规则来执行构建。

目录结构

1
2
3
4
5
6
7
8
/my-cpp-project
├── BUILD # Bazel 构建文件
├── WORKSPACE # Bazel 工作空间文件
├── src/
│ ├── BUILD # C++ 代码的 Bazel 构建文件
│ └── main.cc # C++ 源文件
└── include/
└── myheader.h # C++ 头文件

目标匹配符

: 当前包的所有规则
:all 当前包的所有目标
… 递归匹配所有子包

可以把bazel 和go mod对比

构建命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 构建单个目标
bazel build //src/main:app

# 构建当前目录下的所有目标
bazel build :all

# 构建包内的所有目标
bazel build //src/main/...

# 构建多个指定目标
bazel build //src/main:app //tests:unit_tests

# 构建整个工作区
bazel build //...

# 排除特定目标
bazel build //... -- //experimental/...

bazel的输出

目录名称 用途
bazel-bin 二进制文件(如 cc_binary)
bazel-genfiles 生成的源代码(如协议缓冲区)
bazel-testlogs 测试日志
bazel-out 实际物理存储目录(被符号链接引用)

以googletest的bazel 为例

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
# Google Test including Google Mock
cc_library(
name = "gtest",
srcs = glob(
include = [
"googletest/src/*.cc",
"googletest/src/*.h",
"googletest/include/gtest/**/*.h",
"googlemock/src/*.cc",
"googlemock/include/gmock/**/*.h",
],
exclude = [
"googletest/src/gtest-all.cc",
"googletest/src/gtest_main.cc",
"googlemock/src/gmock-all.cc",
"googlemock/src/gmock_main.cc",
],
),
hdrs = glob([
"googletest/include/gtest/*.h",
"googlemock/include/gmock/*.h",
]),
includes = [
"googlemock",
"googlemock/include",
"googletest",
"googletest/include",
],
linkopts = select({
":qnx": ["-lregex"],
":openbsd": [
"-lm",
"-pthread",
],
"//conditions:default": ["-pthread"],
}),
deps = select({
":has_absl": [
"@abseil-cpp//absl/container:flat_hash_set",
"@abseil-cpp//absl/debugging:failure_signal_handler",
...
],
"//conditions:default": [],
}) + select({
":fuchsia": [
"@fuchsia_sdk//pkg/fdio",
"@fuchsia_sdk//pkg/syslog",
"@fuchsia_sdk//pkg/zx",
],
"//conditions:default": [],
}),


cc_test(
name = "gtest_samples",
size = "small",
srcs = [
"googletest/samples/sample1_unittest.cc",
],
linkstatic = 0,
deps = [
"gtest_sample_lib",
":gtest_main",
],
)

)

select 函数根据特定条件(如是否启用 Abseil 库)选择不同的依赖项。

glob, 选中多个文件

1
2
3
4
5
glob(
include = [文件模式列表],
exclude = [排除模式列表], # 可选
exclude_directories = 1, # 默认排除目录(可选)
)

includes 命令的作用
编译当前包的.cpp文件时自动添加 -I<package_path>/include

1
includes = ["include"],      # 头文件搜索路径(相对于当前包)

linkopts
linkopts 用于指定 ​链接器选项​(Linker Flags),控制如何将目标文件、静态库或动态库链接成最终的可执行文件或共享库

MODULE.bazel 文件中的内容

1
2
3
4
5
6
7
8
9
bazel_dep(
name = "abseil-cpp", # 依赖名称
version = "20250127.0", # 版本号
)

bazel_dep(
name = "platforms",
version = "0.0.10",
)

通过 Bzlmod 直接从 Bazel 中央仓库(Bazel Central Registry, BCR)拉取依赖。

单元测试

google 的gtest库是广泛使用的单元测试框架,本地mock可以使用gmock。

gmock只支持mock虚函数,这个比较坑

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
#include <gmock/gmock.h>
#include "data_service.h"

class MockDataService : public DataService {
public:
MOCK_METHOD(int, fetchData, (int id), (const override));
MOCK_METHOD(void, saveData, (int id, const std::string& data), (override));
};

// data_processor_test.cpp
#include <gtest/gtest.h>
#include "data_processor.h"
#include "data_service_mock.h"

using testing::Return;
using testing::_;
using testing::Throw;

TEST(DataProcessorTest, ProcessHighValue) {
MockDataService mock;
DataProcessor processor(&mock);

// 设置期望:fetchData(42) 返回 200
EXPECT_CALL(mock, fetchData(42))
.WillOnce(Return(200));

// 期望 saveData 被调用一次,参数为 (42, "high")
EXPECT_CALL(mock, saveData(42, "high"));

// 调用被测方法
int result = processor.process(42);
ASSERT_EQ(result, 400); // 200 * 2 = 400
}

TEST(DataProcessorTest, ProcessLowValue) {
MockDataService mock;
DataProcessor processor(&mock);

// 设置期望:fetchData(10) 返回 50
EXPECT_CALL(mock, fetchData(10))
.WillOnce(Return(50));

// 期望 saveData 被调用一次,参数为 (10, "low")
EXPECT_CALL(mock, saveData(10, "low"));

int result = processor.process(10);
ASSERT_EQ(result, 25); // 50 / 2 = 25
}

int main(int argc, char​**​ argv) {
testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}

静态检查和格式化

Clang-Tidy 是一个基于 Clang 的 C++ 静态分析工具,用于执行代码检查、风格检测和代码优化。Clang-Tidy配置文件通常位于项目的根目录,名为 .clang-tidy

1
2
3
4
Checks:          '*, -clang-analyzer-*'
WarningsAsErrors: 'true'
HeaderFilterRegex: '.*'
FormatStyle: file

Clang-Format 用于格式化 C++ 代码,并且支持根据 .clang-format 配置文件自定义格式化规则。

1
2
3
clang-format -i <your-file.cpp>
# 指定风格
clang-format -i -style=google <file>

Valgrind 内存动态检查工具,可以检查内存泄漏、内存泄漏,未初始化内存访问等

1
2
# 检查内存泄漏
valgrind --leak-check=full ./your_program

自动补全

使用clangd实现自动补全,命令行安装sudo apt-get install clangd-10

cmake启用CMAKE_EXPORT_COMPILE_COMMANDS,会在build目录生成compile_commands.json 文件

日志

编码使用的printf 也是一种简单的日志调试,根据输出看预期是否正确。C++日志库也可以选择google的glog。

调试工具

gdb调试普通程序,需要对普通程序编译加-g选项,也就是debug编译。

gdb调试命令

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
# 运行调试器
run
run arg1 arg2

# 设置断点
break main
break 10
break 10
break 10 if x == 5 # 条件断点

# 删除断点
delete
delete 1

# 显示断点
info breakpoints
info breakpoint 1

# 启用和关闭断点
enable 2
disable 2

# 断点继续执行
continue

# 单步执行
step # 进入函数内部执行
next # 会跳过函数调用
finish # 运行直到当前函数执行结束

# 查看线程堆栈
backtrace
frame 1 # 切换栈
up # 切换到当前栈帧的上一层,即父函数的栈帧
down # 切换到当前栈帧的下一层,即子函数的栈帧

# 查看变量值
print x
print my_struct.field
print my_array[2]

# 修改变量值
set variable x = 10

# 查看指定内存地址内容
x/4xw &x

gdb多线程调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 显示线程
info threads

# 切换线程
thread 2

# 查看线程栈
backtrace

# 打印所有线程堆栈
thread apply all backtrace

# 打印所有线程堆栈到文件
set logging on
set logging file <filename> # 执行这两行命令,后面gdb会把结果输出到文件
info threads
thread apply all backtrace

对于release 编译的调试,需要等进程运行产生core后,结合core对二进制文件进行调试。调试命令gdb <二进制文件> <core_file>,core文件和二进制文件必须对应,一般来说需要保证core是调试的二进制文件生成的。

配置linux系统生成core文件

1
2
3
4
ulimit -c
ulimit -c unlimited
# 设置将 core 文件保存在 /tmp/ 目录,并包括程序的名称 (%e) 和进程 ID (%p) 作为文件名。
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern

core文件的调试只能查看进程崩溃时的状态,以下是主要使用的命令

1
2
3
4
5
6
7
8
9
10
11
// 查看进程崩溃时的函数调用栈
backtrace
// 查看当前线程的栈帧和寄存器状态
info locals
info registers

# 打印所有线程堆栈到文件
set logging on
set logging file <filename> # 执行这两行命令,后面gdb会把结果输出到文件
info threads
thread apply all backtrace

gdb 可以通过gdb attach pid附加到进程,执行命令后,gdb会附加到目标进程,并暂停目标进程的执行,不要在线上使用!

性能排查工具

性能排查tips

  1. 首先看cpu和磁盘利用率(或者磁盘吞吐/iops),是否到瓶颈。也就是硬件到瓶颈
  2. 如果1没有到,说明是软件栈瓶颈。需要看用户程序瓶颈还是内核。
  3. 可以使用perf 直接观察软件栈,可以打印火焰图,也可以perf top -p 查看线程情况
  4. 可以用strace 查看系统调用的延迟,判断内核是不是有瓶颈
  5. 如果程序里有链路 trace,可以直接通过 trace 看程序那部分耗时大
  6. 确认出问题的线程后,可以用 pstack 打印线程栈帧,看是否一直出现wait 相关的栈

pstack 打印正在运行进程的堆栈信息,strace打印系统调用信息。多次执行pstack 可以看潜在的用户程序瓶颈(如有无wait调用栈),执行strace可以借助系统调用查看内核是否到瓶颈。

perf 是linux内核提供的强大性能排查工具。

JAVA

JAVA编译和运行

Java编译是将.java 文件转换为字节码(.class 文件),字节码并不是cpu可执行的汇编机器码,平台无关。编译使用java提供的编译器javac执行。

编译后,每个java文件都会产生一个.class文件,类似C++的.o文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 编译.java文件
javac HelloWorld.java
javac Class1.java Class2.java
javac *.java

# -classpath指定编译查找的类路径。
javac -classpath /path/to/library.jar MyProgram.java
javac -classpath /path/to/library1.jar:/path/to/library2.jar MyProgram.java

# -d 指定编译输出.class 文件的目录
javac -d bin MyProgram.java

# -g添加调试信息
javac -g MyProgram.java

运行.class文件,只需要显示运行携带main方法的类,相关的类会自动被加载

1
2
# 不需要加.class后缀
java HelloWorld

调试

jdb是java提供的调试工具, jdb比较反人类的是,它的命令没有简写,例如next不能写作n, cont不能写作c, 以及反人类的命令stop at MyProgram:10

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
# 开始运行
run
# 查看帮助信息
help

# 设置断点
stop at MyProgram:10
stop in MyProgram.myMethod

Usage: stop at <class>:<line_number> or
stop in <class>.<method_name>[(argument_type,...)]

# 查看断点(没错clear是查看断点)
clear
# 删除某个断点,不支持删除全部断点。
clear MyProgram:10

# 断点继续运行
cont

# 单步执行
next
step

# 返回到上层调用,类似gdb的finish
step up

list # 显示旁边代码

# 查看变量
print variableName
print objectInstance.memberVariable

# 查看线程信息
thread # 当前线程信息
thread 1 # 1号线程信息
# 暂停和恢复某线程的运行
suspend [thread id(s)]
resume [thread id(s)]

# 查看线程的栈帧
where # 当前线程信息
where 2

# 查看class和method信息
class <className>
method <>

调试正在运行的进程

1
2
3
4
5
# 列出正在运行的java进程
jps -l

# 连接正在运行的进程
jdb -attach <pid>

测试

JUnit 是最常用的 Java 单元测试框架,使用注解 @Test 来标记测试方法,以及 @Before 和 @After 来标记测试前后的初始化和清理方法。

Go

编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 编译单个文件
go build main.go

# 编译整个包
go build

# 编译参数, -gcflags
go build -gcflags "-N" main.go

-N # 禁用优化
-l # 禁用内联优化
-l -N # 禁用优化和内联
-m # 输出优化决策信息
-d # 增加调试信息

调试

golang推荐使用Delve 进行调试,安装go install github.com/go-delve/delve/cmd/dlv@latest

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
# 调试单个文件
dlv debug main.go
# 调试某个目录
dlv debug

# 设置断点
break b
b main.go:10
# 显示已经设置的断点
breakpoints (alias: bp)

# 栈帧移动
down ------------------------ Move the current frame down.
up -------------------------- Move the current frame up.

# 继续运行直到下一个断点
continue c

# 单步执行
next n # 单步执行,跳过函数调用
step s # 单步进入函数内部

# 打印变量值
print p
p x

# 显示当前代码行及上下文
list 或 ls

goroutine ------------------- Shows or changes current goroutine
goroutines # 查看当前所有 Goroutines
# 查看goroutine堆栈
stack <goroutine_id>

threads 显示所有线程信息
thread (alias: tr) ---------- Switch to the specified thread.

静态代码检查

gopls,通常会随 Go 扩展自动安装。可以和vscode结合配置

1
2
3
4
5
6
7
8
9
10
11
# 静态检查
gopls check <path-to-your-directory-or-file>
# 格式化代码
gopls format <path-to-your-file>
# 代码补全
gopls completion <path-to-your-file>:<line>:<column>

# 跳转到函数定义
gopls definition <path-to-your-file>:<line>:<column>
# 跳转到引用
gopls references <path-to-your-file>:<line>:<column>

单元测试

Go 自带的测试框架(testing 包)支持单元测试和性能测试。测试文件以 _test.go 结尾, 测试函数必须以 Test 开头,后面跟随测试的函数名。

1
2
3
4
5
6
7
8
9
10
# 运行测试
go test
go test -run TestAdd # 只运行特定的测试函数
go test -cover # 查看覆盖率

# 调试测试
dlv test

# 调试单个测试
dlv test -- -test.run TestFunctionName

Python

编译和运行

Python 会将源代码编译为一种字节码(Bytecode),存储为 .pyc 文件(位于 __pycache__ 文件夹)。字节码会被 Python 虚拟机(PVM,Python Virtual Machine)翻译为底层的机器指令执行。

相比java通常把字节码打包成jar,后续由jvm执行; python一般直接保留源代码,python虚拟机直接执行源代码。python编译过程也不会进行类型检查,编译器优化行为少。

python语法比较灵活,表达式函数可以在全局执行,执行实现先于main模块。

调试

python -m pdb your_program.py 启用pdb调试

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
# 单步运行
n next # n
s step # 进入当前行中的函数调用,逐步执行
c continue # 继续执行程序,直到下一个断点
r return # 运行到当前函数完毕

# 清理断点
clear
clear filename:lineno # 删除某行所有断点
clear number # 删除编号断点

enable bpnumber # 启动和关闭断点
disable bpnumber

# 打印变量值或表达式
p <expression>

# 列出当前行附近的代码
l (list)

# 设置断点
b break # 显示已经设置的断点
b <line_number>
b 12
b add

# 显示当前的调用栈
w (where)
up # 切换到调用栈的上一层(即父函数)
down # 切换到调用栈的下一层(即子函数)

<expression> # 执行表达式,可以修改变量
a = 10

静态检查

pylint 工具检查代码,执行

1
2
3
4
5
pylint <your-python-file.py>
pylint <your-project-folder>

# 生成pylint的静态检查规则
pylint --generate-rcfile > .pylintrc

pylint不支持自动格式化,Black可以用来做python自动化代码格式化工具

单元测试

unittest 库,导入待测试的模块,对需要测试的模块进行单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import unittest
import math_utils

class TestMathUtils(unittest.TestCase):
def test_add(self):
# 测试 add 函数
self.assertEqual(math_utils.add(1, 2), 3)
self.assertEqual(math_utils.add(-1, 1), 0)
self.assertEqual(math_utils.add(0, 0), 0)

def test_subtract(self):
# 测试 subtract 函数
self.assertEqual(math_utils.subtract(10, 5), 5)
self.assertEqual(math_utils.subtract(0, 1), -1)
self.assertEqual(math_utils.subtract(100, 50), 50)

if __name__ == "__main__":
unittest.main()

日志

配置日志处理器

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
import logging

# 创建日志器
logger = logging.getLogger("my_logger")
logger.setLevel(logging.DEBUG)

# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)

# 创建文件处理器
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.WARNING)

# 定义日志格式
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

# 添加处理器到日志器
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# 测试日志
logger.debug("这是一条调试日志")
logger.info("这是一条一般信息日志")
logger.warning("这是一条警告日志")
logger.error("这是一条错误日志")
logger.critical("这是一条严重错误日志")