跳到主要内容

91 篇博文 含有标签「后端」

后端开发技术文章

查看所有标签

Go 使用 Swagger

· 阅读需 2 分钟
素明诚
Full stack development

安装 Swagger

使用 Go 的包管理工具安装 swag 命令行工具,这个工具用于自动生成 Swagger 文档的相关文件。

go get -u github.com/swaggo/swag/cmd/swag

在 bin 目录安装

go install github.com/swaggo/swag/cmd/swag@latest

生成 Swagger 文档

在项目的根目录下运行 swag init 命令。这将会扫描你的 Go 源代码文件(通常是查找特定的注解格式),并在项目中生成一个 docs 文件夹,里面包含 swagger.jsonswagger.yamldocs.go

swag init

安装 Gin 与 Swagger 的集成库

需要安装 swaggo/gin-swaggerswaggo/files 包,用于在 Gin 应用程序中提供 Swagger UI。

go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files

配置 Gin 路由以提供 Swagger UI

修改你的 Gin 应用程序代码,引入 Swagger 文档生成的包,并设置一个路由来提供 Swagger UI。

package main

import (
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
_ "your_project/docs" // 替换 your_project 为实际的项目名或路径 必须导入
)

func main() {
r := gin.Default()

// 设置 Swagger 的路由
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

// 运行 Gin
r.Run()
}

注解你的 API

在你的 API 控制器或处理器函数上添加必要的注解,以便 swag 能够识别并生成正确的 API 文档。

// @Summary 概述你的接口
// @Description 详细描述接口功能
// @Tags 使用适当的标签
// @Accept json
// @Produce json
// @Param name query string true "参数描述"
// @Success 200 {object} YourResponseStruct "成功返回的结构体描述"
// @Router /your_route [get]
func YourHandler(c *gin.Context) {
// 实现功能
}

重新生成文档并测试: 每次更新注解后,你需要重新运行 swag init 以更新 Swagger 文档。然后启动你的 Gin 应用程序,访问 Swagger UI 确认 API 文档是否正确。

4bab4dcdcd0c2d2c95b45cbd07d14fbb## 仓库地址
https://github.com/swaggo/swag

Go 语言的 go get 命令时禁用模块的哈希校验功能

· 阅读需 2 分钟
素明诚
Full stack development

GOSUMDB 环境变量

GOSUMDB 环境变量用于指定用于验证模块内容的 Go checksum database 的服务器。Go 模块系统使用这个数据库来确保模块内容的一致性和安全性,防止模块遭到篡改。

禁用哈希校验

设置 GOSUMDB=off,可以关闭对所有外部模块的校验和数据库查询。这意味着当你使用 go get 下载依赖时,Go 工具链不会尝试验证下载的模块是否与校验和数据库中记录的哈希值相匹配。

这种设置通常用于私有网络或者是对安全要求不高且需要提速的开发环境中。

设置方法

在命令行中设置 GOSUMDB=off 可以通过以下命令实现

go env -w GOSUMDB=off

这个命令会在当前用户的 Go 环境中永久设置 GOSUMDB 的值为 off

提示~

禁用模块哈希校验将降低项目安全性,因为你将无法验证模块的内容是否被篡改。只有在你完全信任你的模块来源,或在内部、隔离的开发环境中使用时,才推荐使用这种方法。

使用场景

在内部或私有的网络环境中,无法访问 Go 的官方 checksum database。

开发过程中需要频繁测试或更新依赖,且对安全性的即时验证要求较低。

JWT 签名和验证签名的过程

· 阅读需 3 分钟
素明诚
Full stack development

生成(签名)JWT

确定 Header:Header 通常包含两部分信息:使用的算法(比如 HS256 表示 HMAC SHA-256)和 Token 类型(通常是 JWT)。

{
"alg": "HS256",
"typ": "JWT"
}

构建 Payload:Payload 包含 claims,这些 claims 可以是注册的 claims、公共的 claims 或私有的 claims

{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

签名:使用 Header 中指定的算法和一个密钥对 Header 和 Payload 进行签名。签名的目的是验证 Token 在传输过程中未被篡改。

例如,如果使用 HMAC SHA-256,签名过程将是

  HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

组合 JWT:将编码后的 Header、Payload 和签名组合成一个 JWT。格式通常为header.payload.signature

JWT 验证过程

分解 JWT:首先,从 JWT 字符串中分离出 Base64 编码的 Header、Payload 和 Signature。JWT 通常形如header.payload.signature

Base64 解码:将编码的 Header 和 Payload 解码回它们原始的 JSON 格式的字符串。

重新生成签名:使用相同的算法(Header 中指定的,如 HMAC SHA-256)和secret对解码后的 Header 和 Payload 重新生成签名。具体步骤如下:

  1. 将解码后的 Header 和 Payload 重新转换成 Base64 编码的字符串。
  2. 将这两个 Base64 字符串用点.连接起来形成一个新的字符串。
  3. 使用secret作为密钥,对这个字符串进行 HMAC SHA-256 运算,生成新的签名。

比较签名:将重新生成的签名与 JWT 中原来包含的签名进行比较。如果两者相同,则认为 JWT 是有效的,没有被篡改,且是由掌握secret的实体发行的。

Secret 的作用

在这个验证过程中,secret起到的是保证签名真实性和数据未被篡改的关键角色:

  1. 签名的安全性:只有知道secret的实体才能正确生成和验证签名。这意味着即使有人截获了 JWT,但没有secret,他们也无法生成新的有效签名,从而无法伪造有效的 JWT。
  2. 数据的完整性:通过比较重新生成的签名和 JWT 中的签名,可以确认数据在传输过程中未被修改。因为任何对 Header 或 Payload 的改动都会导致新生成的签名与原签名不匹配。

检查 Claims:验证签名之后,还需要检查 Payload 中的 claims,如exp(过期时间)来确定 Token 的有效性。

注意

虽然 JWT 的 Header 和 Payload 是使用 Base64 编码的,这种编码是可逆的,因此任何人都可以看到内容。如果需要保密 Payload 中的数据,应考虑对 Payload 进行加密,这种情况下通常使用 JWE(JSON Web Encryption)。

Express 中使用 Winston 记录日志

· 阅读需 1 分钟
素明诚
Full stack development

首先,你需要在你的 Node.js 项目中安装 Winston 和 winston-daily-rotate-file(实现日志滚动

npm install winston
npm install winston-daily-rotate-file

示例:编写一个 logger 实例导出使用

import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import fs from 'fs';
import path from 'path';

function ensureLogDirectory() {
const logDirectory = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory, {recursive: true});
}
return logDirectory;
}

// 创建日志文件传输配置
function createFileTransport(logDirectory) {
return new DailyRotateFile({
filename: path.join(logDirectory, 'admin-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
winston.format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
),
});
}

// 创建控制台传输配置
function createConsoleTransport() {
return new winston.transports.Console({
level: 'debug',
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
});
}

const logDirectory = ensureLogDirectory();
const transport = createFileTransport(logDirectory);
const consoleTransport = createConsoleTransport();

// 初始化并导出logger
export const logger = winston.createLogger({
transports: [
transport,
consoleTransport
]
});

编写中间件

export const logMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const logLevel = res.statusCode >= 400 ? 'error' : 'info';
const message = `${req.method} ${req.url} - Status Code: ${res.statusCode} - ${duration}ms `;
logger[logLevel](message);
});
next();
};

注册中间件

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('dist'));
app.use(logMiddleware); // 这里

效果

ca9b705e4e51a672c2938357f93fd824## 项目地址
https://github.com/winstonjs/winston

如何编写 Go 语言的单元测试

· 阅读需 2 分钟
素明诚
Full stack development

testing 包

testing 包是 Go 语言的标准库组成部分,专门用于支持自动化测试。该包提供了丰富的功能,允许开发者定义测试用例、记录测试结果,并利用一系列工具函数控制测试逻辑和报告测试状态。这包括处理失败、日志记录和条件性测试中断等功能。

示例代码

以一个基本的加法函数测试为例。首先,假设你有一个加法函数定义在 calculator.go 文件中

package calculator

// Add 返回两个整数的和
func Add(a, b int) int {
return a + b
}

对应的测试文件 calculator_test.go 包含如下代码

package calculator

import "testing"

// TestAdd 测试 Add 函数
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1, 2) = %d; expected %d", result, expected)
}
}

编写测试的规则

测试文件名应以 _test.go 结尾,存放在与被测试源文件相同的包中。每个测试函数必须以 Test 开头,并接受一个 *testing.T 类型的参数。这些函数可以使用 testing 包提供的方法进行错误报告和日志记录

  • t.Errorf()t.Error(): 当测试需要报告错误时使用,Errorf 允许按格式输出。
  • t.Fatalf()t.Fatal(): 遇到无法继续的错误时调用,会立即终止测试。
  • t.Log()t.Logf(): 用于记录测试信息,这些信息在使用 -v 参数运行测试时显示。

运行测试

打开命令行或终端,在包含测试文件的目录下运行

go test

使用 -v 参数可以查看每个测试函数的详细输出信息。若只想运行特定的测试,可以使用 -run 参数,如

go test -run TestAdd

如何编写 Go 语言的性能测试

· 阅读需 1 分钟
素明诚
Full stack development

testing 包

testing 包提供了一个专门用于性能测试的工具,称为基准测试(benchmarks)。基准测试函数以 Benchmark 开头,并接受一个 *testing.B 类型的参数。这个参数提供了控制测试运行的方法和属性,例如 b.N,它代表测试循环的次数。

示例代码

package main

import (
"encoding/json"
"github.com/bytedance/sonic"
)

type Student struct {
Name string
Age int
Gender string
}

type Class struct {
Id string
Students []Student
}

var (
s = Student{"张三", 18, "女"}
c = Class{
Id: "1年2班",
Students: []Student{s, s, s},
}
)
// Benchmark 开头
func
Json(b *testing.B) {
for i := 0; i < b.N; i++ {
bytes, _ := json.Marshal(c)
var c2 Class
json.Unmarshal(bytes, &c2)
}
}

func BenchmarkSonic(b *testing.B) {
for i := 0; i < b.N; i++ {
bytes, _ := sonic.Marshal(c)
var c2 Class
sonic.Unmarshal(bytes, &c2)
}
}

IDE 执行

44c617759eee959e35e5c76c15fb67df### 命令行执行

打开命令行或终端,在包含性能测试文件的目录下运行:

go test -bench=.

使用 -bench 参数可以指定要运行的基准测试。-bench=. 表示运行所有基准测试。

Go fmt 格式化输出

· 阅读需 2 分钟
素明诚
Full stack development

基本格式化占位符

占位符描述
%v默认格式输出变量的值。结构体时输出字段值。
%+v输出结构体时包括字段名和字段值。
%#v输出值的 Go 语法表示,即源代码片段。
%T输出值的类型。
%d, %b, %o, %x, %X分别表示整数的十进制、二进制、八进制、小写十六进制和大写十六进制格式。
%f, %e, %E, %g, %G分别表示浮点数的默认格式、小写和大写的科学计数法、最短表示法(自动选择 %e 或 %f)。
%s输出字符串。
%q输出双引号围绕的由 Go 语法安全转义的字符串。
%p输出指针的地址(十六进制)。
%c输出字符(Unicode 码点)。

格式控制选项

选项描述
%9d宽度为 9 的十进制数,右对齐。
%.2f浮点数,小数点后只保留两位。
%9.2f, %-9.2f宽度为 9,小数点后保留两位的浮点数,分别右对齐和左对齐。
%09d宽度为 9 的十进制数,空白处用零填充。
%+d输出整型数时,显示符号(正数显示 +,负数显示 -)。
% 6d宽度至少为 6,不足部分以空格填充。

注意事项

类型匹配:确保占位符与变量类型匹配,防止类型错误。

精度与宽度:合理设置精度和宽度,注意过小的宽度不会截断输出,而过小的精度可能导致数据丢失。

性能考虑:频繁使用字符串格式化会影响程序性能,尤其是在数据处理和循环中。

Go 语言中 defer 语句

· 阅读需 1 分钟
素明诚
Full stack development

Go 语言中,defer 语句遵循后进先出(LIFO)的原则执行。这样做主要是为了简化资源(如文件、锁)的管理,使得资源的获取和释放顺序保持一致,易于维护。

package main

import (
"fmt"
)

func main() {
defer fmt.Println("第一个 defer") // 最后执行
defer fmt.Println("第二个 defer") // 第二个执行
defer fmt.Println("第三个 defer") // 最先执行

fmt.Println("主体")
}

本质原因就是因为 defer 语句使用了栈来存储

defer 与 匿名函数

使用 defer 匿名函数时,核心问题在于变量捕获特性

package main

import "fmt"

func main() {
a := 1
defer func() {
fmt.Println(a) // 输出 2
}()
a = 2
fmt.Println(a) // 输出 2
}

defer 后面如果是 go 语句,这个 go 语句里面的变量,在注册的时候就已经计算好了

defer 后面如果是匿名函数,这个匿名函数设计到的变量,是在执行的时候才会计算。

Go 语言中切片的声明初始化和操作详解

· 阅读需 2 分钟
素明诚
Full stack development

声明切片变量

声明:当声明一个切片变量如 var s []int 时,s 初始化为 nil,表示它没有指向任何底层数组。其长度 (len) 和容量 (cap) 都为 0,无法访问或修改任何元素,因为实际上还没有底层数组与之关联。

使用 make 函数创建切片

内存分配:调用 make([]int, 3, 5) 时,底层会为切片分配一个长度(容量)为 5 的整型数组。这个数组是在堆上分配的,以便动态管理内存,允许切片在运行时调整大小。

初始化切片结构:切片结构体的实例会在栈上创建,包含三个关键的字段

  • Pointer:指向已分配底层数组的第一个元素的指针。
  • Len:设置为指定的长度,此例中为 3,意味着切片当前访问到的元素数量。
  • Cap:设置为数组的总容量,此例中为 5,即最多可以扩展到的元素数量。

初始化数组元素:新分配的数组元素初始化为其类型的零值。对于 int 类型,每个元素都初始化为 0

使用字面量初始化切片

内存分配:使用字面量如 s := []int{1, 2, 3} 初始化切片时,底层同样会在堆上分配一个容纳 3 个整型元素的数组。

初始化切片结构:创建的切片结构体实例指向这个数组,长度和容量都设置为字面量中元素的数量,即 3。

填充数组元素:数组中的元素将直接使用提供的字面值(1, 2, 3)进行初始化,不使用零值。

切片操作

创建新切片:当执行 s2 := s[1:3] 的切片操作时,底层不会复制数组,而是创建一个新的切片结构体,指向原切片 s 的底层数组的第 2 个元素(索引 1)。

新切片属性

  • Len:新切片的长度为 2,因为它从索引 1 到索引 2,包含 2 个元素。
  • Cap:新切片的容量为 4,计算方式为原切片的容量 5 减去新切片的起始索引 1

Linux 运行 go 文件

· 阅读需 1 分钟
素明诚
Full stack development
package main

import "fmt"

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

直接运行

go run hello.go

编译后运行

go build hello.go
./hello

直接运行和编译后运行的区别

直接运行 (go run): 这个命令会在内部先编译程序成一个临时文件,然后立即执行这个编译后的文件。它适合于开发过程中快速测试和调试代码,因为它省去了手动编译的步骤。

编译后运行 (go build): 这个命令会将你的 Go 源代码编译成一个可执行文件。在编译过程中,Go 编译器会优化代码,从而使最终的可执行文件在运行时拥有更好的性能。编译完成后,你可以多次运行这个可执行文件而无需再次编译,这对于生产环境非常重要。