跳到主要内容

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

后端开发技术文章

查看所有标签

Nginx 代理 EventsStream 接口配置优化

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

处理 HTTP 流(如 SSE-Server Sent Events)的场景,这种情况下,配置需要优化以支持长连接和实时数据流

配置优化

proxy_buffering off;

这个设置非常适合事件流。对于 SSE 或 WebSocket 等实时数据传输技术,禁用缓冲可以减少发送消息到客户端的延迟,因为它允许服务器立即发送响应而不是等待缓冲区填满。

chunked_transfer_encoding on;

适合事件流。对于动态生成且大小未知的内容(如实时生成的事件数据),启用分块传输可以持续地发送数据到客户端,而不必等待所有内容生成完成。

tcp_nopush on;

处理事件流时可能不是最佳选择。虽然它可以优化数据包的传输效率,但对于需要尽快把小消息推送到客户端的实时通信来说,可能会引入不必要的延迟。

tcp_nodelay on;

非常适合事件流。此设置确保数据包立即发送,对于需要低延迟的实时数据传输(如 SSE)非常关键。

keepalive_timeout 120;

适合事件流。长时间的 keepalive 超时对于维持 SSE 这样的长连接非常有利,因为它可以减少因连接超时而频繁重连的情况。

配置

建议只是针对需要设置的接口进行配置如下

    location /api/chat-process {
# 后台接口地址
proxy_pass http://127.0.0.1:5008/api/chat-process;
proxy_redirect default;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
proxy_buffering off;
chunked_transfer_encoding on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 300;
}

Go Gin 封装 translations 插件

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

translate.go

package util

import (
"errors"
"fmt"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTranslations "github.com/go-playground/validator/v10/translations/en"
chTranslations "github.com/go-playground/validator/v10/translations/zh"
"strings"
)

var trans ut.Translator

func init() {
if err := transInit("zh"); err != nil {
LogRus.Fatal("Failed to initialize translator:", err)
}
}

func transInit(locale string) error {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
zhT := zh.New() // Chinese translator
enT := en.New() // English translator
uni := ut.New(enT, zhT, enT) // Universal translator

var ok bool
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
}

// Register translation based on locale
switch locale {
case "zh":
return chTranslations.RegisterDefaultTranslations(v, trans)
case "en":
return enTranslations.RegisterDefaultTranslations(v, trans)
default:
return enTranslations.RegisterDefaultTranslations(v, trans)
}
}

return fmt.Errorf("failed to assert Validator")
}

func TranslateErrors(err error) string {
var errs validator.ValidationErrors
if errors.As(err, &errs) {
var errMessages []string
for _, e := range errs {
translatedMsg := e.Translate(trans)
errMessages = append(errMessages, translatedMsg)
}
return strings.Join(errMessages, ", ")
}
return err.Error()
}

Login.go

直接使用封装好的 TranslateErrors 进行翻译

func Login(ctx *gin.Context) {
// 未登录,登录流程
var req LoginRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, LoginResponse{Code: 1, Msg: util.TranslateErrors(err)})
return
}
}

Go Gin 常用的验证规则

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

在 Gin 框架中,数据验证是通过集成的 go-playground/validator 包来实现的。这个包支持许多内置的验证规则,可以用于验证输入数据的各种条件。

常用规则

标签描述
required字段必须有值,不能为默认零值或空
len字段长度必须等于指定的值
min对于数值,最小值必须不小于指定值;对于字符串或数组,长度必须不小于指定值
max对于数值,最大值必须不大于指定值;对于字符串或数组,长度必须不大于指定值
eq字段值必须等于指定的值
ne字段值必须不等于指定的值
lt字段值必须小于指定的值
lte字段值必须小于或等于指定的值
gt字段值必须大于指定的值
gte字段值必须大于或等于指定的值
email字段值必须是有效的电子邮件地址格式
url字段值必须是有效的 URL 格式
alnum字段必须是字母数字字符
numeric字段必须是有效的数字
hexadecimal字段必须是十六进制格式
hexcolor字段必须是有效的十六进制颜色代码
rgb字段必须是有效的 RGB 颜色
rgba字段必须是有效的 RGBA 颜色
hsl字段必须是有效的 HSL 颜色
hsla字段必须是有效的 HSLA 颜色
e164字段必须是有效的 E.164 格式的电话号码
oneof字段值必须是指定列表中的一个
uuid字段值必须是有效的 UUID
uuid3字段值必须是有效的 UUID3 格式
uuid4字段值必须是有效的 UUID4 格式
uuid5字段值必须是有效的 UUID5 格式
ascii字段值必须是 ASCII 字符
printascii字段值必须是可打印的 ASCII 字符

Gin 中使用验证规则

安装 Gin

go get -u github.com/gin-gonic/gin

使用验证规则

type LoginRequest struct {
Username string `form:"username" binding:"required"`
Password string `form:"password" binding:"required,min=6"`
}

处理请求:在你的路由处理函数中,使用 ShouldBindShouldBindJSONShouldBindQuery 等方法来绑定请求数据并触发验证

func login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 处理登录逻辑...
}

参考链接

https://github.com/go-playground/validator

go get 命令详解

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

常用命令

-d:下载模块到本地系统,但不安装。这通常用于仅下载源码而不进行编译的场景。

-u:更新已有的模块及其依赖到新版本。这包括直接依赖和间接依赖。

  • -u=patch:限制更新到补丁版本,即在主要和次要版本号不变的情况下更新到最新修订版。
  • -u=minor:更新到允许的最新次要版本,可能会包括主要版本号不变的新功能。

-t:除了获取包本身,还会获取包用于测试的依赖。

-insecure:允许使用不安全的协议,如 HTTP,进行网络操作。这在访问某些未使用 HTTPS 的私有仓库时可能需要,但通常不推荐因安全风险。

-v:显示详细的操作信息

版本控制参数

可以指定获取特定版本、分支或修订版的包。

  • 例如:go get foo@v1.2.3go get foo@latestgo get foo@branch-name

随着 Go 语言的版本更新,go get 的功能在 Go 1.16 版本以后有所变化,逐渐转向主要用于旧的 GOPATH 模式下的包管理,而在模块模式下(即使用 go.mod),更推荐使用 go install 来安装可执行文件。以下是 Go 1.16 之后,针对模块模式的 go get 行为:

  • 在 Go 模块模式下,go get 主要用于添加依赖到当前模块或更新模块和它们的依赖。
  • go install pkg@version 安装特定版本的可执行模块。

什么是服务发现

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

服务注册和服务查找

服务注册

每个微服务在启动时主动向服务注册中心注册自己的关键信息,如 IP 地址、端口号、服务名称等。这个过程确保服务的可发现性,并在服务状态改变时更新注册信息。

服务查找

当一个服务需要与另一服务交互时,它通过服务注册中心查询所需服务的可用实例和地址信息。这个查询依赖于服务注册数据。

Go 语言实现服务发现

Consul:Consul 是一个提供服务发现、配置和分段功能的工具,它通过一个简单的 HTTP API 为服务注册和服务发现提供支持。Go 可以通过 HashiCorp 提供的 Consul API 客户端库与 Consul 交互

Etcd:Etcd 是一个键值存储仓库,用于配置共享和服务发现。Etcd 的客户端库为 Go 语言提供了接口,可以很方便地集成到 Go 微服务中

Eureka:虽然 Eureka 主要是 Netflix 开发并且与 Java 生态系统结合更紧密,但也有 Go 客户端可以与 Eureka 服务注册和发现机制交互。

Go 语言微服务示例

Go 中使用 Consul 进行服务注册和发现

package main

import (
"github.com/hashicorp/consul/api"
"log"
)

func main() {
// 创建一个 Consul 客户端
client, err := api.NewClient(api.DefaultConfig())
if err != nil {
log.Fatal(err)
}

// 服务注册
registration := new(api.AgentServiceRegistration)
registration.ID = "service1"
registration.Name = "userservice"
registration.Port = 8080
registration.Address = "127.0.0.1"
client.Agent().ServiceRegister(registration)

// 服务发现
services, err := client.Agent().Services()
if err != nil {
log.Fatal(err)
}
for _, service := range services {
log.Println("Service:", service.Service, "Address:", service.Address, "Port:", service.Port)
}
}

Go 全局配置的四种方式

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

常量 (Constants)

对于不会改变的配置项,常量是最好的选择

package config

// 定义全局常量
const (
ServerPort = 8080
DatabaseURI = "localhost:5432"
Environment = "production"
)

Map

如果需要运行时可以改变的配置项,Map结构体更好

package config

// 使用map来存储配置
var GlobalConfig = map[string]interface{}{
"timeout": 30,
"logLevel": "info",
"maxConnections": 100,
}

func init() {
// 这里可以根据不同环境或其他逻辑动态调整配置
if Environment == "development" {
GlobalConfig["logLevel"] = "debug"
}
}

Flag

Flag 适用于需要由最终用户在程序启动时配置的参数

package main

import (
"flag"
"fmt"
)

var (
port = flag.Int("port", 8080, "Server port number")
logLevel = flag.String("logLevel", "info", "Logging level")
)

func main() {
flag.Parse() // 解析传入的命令行参数
fmt.Printf("Server will start at port: %d with log level: %s\n", *port, *logLevel)
}

通过命令行传入

$ go run program.go -port=9090

结构体(构造实例)

对于需要良好组织的大型应用配置,结构体提供了最佳的类型安全性和可维护性

package config

type Config struct {
Port int
DatabaseURI string
LogLevel string
}

// NewConfig 创建一个新的Config实例
func NewConfig() *Config {
return &Config{
Port: 8080,
DatabaseURI: "localhost:5432",
LogLevel: "info",
}
}

var GlobalConfig = NewConfig()

func init() {
// 可以根据需要在这里调整配置
if Environment == "development" {
GlobalConfig.LogLevel = "debug"
}
}

以上是使用 Go 解决配置文件的方式,实际使用推荐 viper 和 godotenv

Go 的v v v 有什么区别

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

这里的 v 代表的是 “value”

%v

最基本的格式占位符,用于以默认的方式输出变量的值对于大多数变量类型来说,%v 会输出值的自然形式

%+v

对于结构体(struct)来说,%+v 不仅会输出结构体的值,还会包括结构体的字段名这对于调试或显示更详细的信息是非常有用的

%#v

这个格式占位符会输出值的 Go 语法表示也就是说,输出的格式可以直接作为源代码片段使用这对于调试复杂的数据结构特别有用,因为它可以展示如何使用代码来重建当前的值

这三者的区别如下

package main

import (
"fmt"
)

type Person struct {
Name string
Age int
}

func main() {
p := Person{"John Doe", 30}
fmt.Printf("Using %%v: %v\n", p)
fmt.Printf("Using %%+v: %+v\n", p)
fmt.Printf("Using %%#v: %#v\n", p)
}

输出是

Using %v: {John Doe 30}
Using %+v: {Name:John Doe Age:30}
Using %#v: main.Person{Name:"John Doe", Age:30}

不同的格式占位符对于输出格式有着明显的影响,特别是在处理结构体时

悲观锁和乐观锁的区别

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

悲观锁和乐观锁是数据库管理和并发编程中用来控制数据一致性防止数据冲突的两种策略

悲观锁(Pessimistic Locking)

  • 核心 悲观锁假设最坏的情况,认为数据在并发修改时一定会发生冲突,因此在数据处理前就锁定数据,直到事务完成才释放锁
  • 应用场景 适用于写操作多、冲突概率高的环境,如银行账户余额更新

乐观锁(Optimistic Locking)

  • 核心 乐观锁假设冲突发生的可能性较低,因此不会立即锁定数据,而是在数据提交更新时检查数据在读取后是否被修改过(通常通过版本号或时间戳实现)
  • 应用场景 适用于读操作多、冲突概率低的环境,如一些只偶尔更新的数据查询系统

为什么会有这两种锁

这两种锁的存在是为了处理并发情况下的数据一致性和系统性能的平衡 悲观锁提供较强的数据一致性保证,但可能降低系统的并发性能;乐观锁在并发性能上表现更好,但在高冲突环境下可能导致更多的重试和失败 选择哪种锁策略取决于系统的具体需求和预期的并发冲突情况

两种锁在 Go 中的实现方式

悲观锁

在 Go 中,悲观锁通常可以通过使用 sync.Mutexsync.RWMutex 来实现 这些锁在访问共享资源前必须先获得锁,并在访问完成后释放锁 这样可以确保同一时间内只有一个 goroutine 能修改该资源

package main

import (
"fmt"
"sync"
)

var (
balance int
mutex sync.Mutex
)

func Deposit(amount int) {
mutex.Lock() // 获取锁
balance += amount // 修改共享资源
mutex.Unlock() // 释放锁
}

func main() {
balance = 100
fmt.Println("Initial balance:", balance)
Deposit(50)
fmt.Println("New balance:", balance)
}

使用 sync.Mutex 来确保在修改账户余额时不会发生数据竞争 这是一种悲观锁的实现,因为它假设必然会有冲突

乐观锁

乐观锁在 Go 中可以通过原子操作或自定义逻辑来实现

package main

import (
"fmt"
"sync"
"sync/atomic"
)

var (
balance int64
)

func Deposit(amount int64) {
for {
oldBalance := atomic.LoadInt64(&balance) // 读取当前余额
newBalance := oldBalance + amount // 计算新余额
// 尝试更新余额,如果期间余额未被其他 goroutine 修改,则更新成功
if atomic.CompareAndSwapInt64(&balance, oldBalance, newBalance) {
return
}
// 如果数据被修改过,循环重试
}
}

func main() {
atomic.StoreInt64(&balance, 100) // 初始余额
fmt.Println("Initial balance:", balance)
Deposit(50)
fmt.Println("New balance:", balance)
}

使用 atomic.CompareAndSwapInt64 函数来确保在更新余额时,如果没有其他 goroutine 修改过余额,则更新操作会成功 如果余额在更新前被其他 goroutine 修改了,则重试操作,直到成功 这是乐观锁的一种实现方式,它假设冲突发生的可能性较低

Go 语言的 init 函数执行顺序

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

单个 Go 文件中的多个 init 函数

一个 Go 文件内可以包含多个 init 函数,这些函数将按照它们在文件中的出现顺序被执行这一点对于初始化本地资源或配置特别有用

示例代码

// 文件config.go
package config

import "fmt"

func init() {
fmt.Println("初始化数据库连接")
}

func init() {
fmt.Println("加载应用配置")
}

输出结果

初始化数据库连接
加载应用配置

同一 package 中的多个 Go 文件中的 init 函数

如果一个 package 包含多个 Go 文件,那么各文件中的 init 函数将按文件名的字母顺序执行这对于需要跨文件初始化顺序的场景非常关键

示例代码

// 文件a_init.go
package setup

import "fmt"

func init() {
fmt.Println("Setup part 1")
}

// 文件b_init.go
package setup

func init() {
fmt.Println("Setup part 2")
}

输出结果

Setup part 1
Setup part 2

不同 package 的 init 函数执行顺序

当一个 package 导入其他多个 package 时,被导入的 package 的 init 函数将按照 import 语句的顺序执行,再执行当前 package 的 init 函数这有助于管理复杂的依赖关系

示例代码

// 文件database.go
package database

import "fmt"

func init() {
fmt.Println("Database initialized")
}

// 文件server.go
package main

import (
"fmt"
"sumingcheng.com/project/database" // 路径假设为例子
)

func init() {
fmt.Println("Server setup")
}

func main() {
fmt.Println("Server starting")
}

输出结果

Database initialized
Server setup
Server starting

非 main package 的 init 函数执行顺序

即便是在非 main package 中,init 函数也是在包的任何其他代码执行前执行,包括 main 函数这保证了无论程序的入口点在哪里,所有必要的初始化都能先行完成

示例代码

// 文件logger.go
package logger

import "fmt"

func init() {
fmt.Println("Logger setup complete")
}

// 文件main.go
package main

import (
"sumingcheng.com/project/logger" // 假设的路径
)

func main() {
fmt.Println("Main function running")
}

输出结果

Logger setup complete
Main function running

修改 Go Module 代理

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

bash 设置 Go 代理

要设置 Go 代理,你可以使用 go env 命令来设置环境变量。例如,使用七牛云的 Go 代理可以通过以下命令设置:

export GOPROXY=https://goproxy.cn,direct

这条命令设置了 Go 代理服务器地址为 https://goproxy.cn,并且如果在该代理服务器上找不到所需模块,Go 工具链会尝试直接连接源服务器(direct)。

设置官方代理

export GOPROXY=https://proxy.golang.org,direct

检查设置

go env GOPROXY

PowerShell(管理员) 设置 Go 代理

setx GOPROXY "https://goproxy.cn,direct" /M

检查设置

echo %GOPROXY%

七牛云 Go Module 代理

地址https://goproxy.cn

特点:七牛云提供的服务,稳定可靠,被广泛推荐使用。

阿里云 Go Module 代理

地址https://mirrors.aliyun.com/goproxy/

特点:由阿里云提供,服务稳定,接入阿里云生态。

腾讯云 Go Module 代理

地址https://mirrors.cloud.tencent.com/go/

特点:由腾讯云提供,服务稳定,速度快。

注意事项

确保你使用的 Go 版本支持模块(Go 1.11 及以上版本)。

设置代理后,所有通过 go get 等命令获取的 Go 包都会通过代理服务器下载。

Go 语言的官方包文档网站

https://pkg.go.dev

包搜索功能:用户可以通过搜索框输入包名或关键词来查找对应的 Go 包。

文档浏览:为每个包提供详细的 API 文档,包括函数、类型、变量等的说明。

版本历史:展示包的版本历史和各个版本的详细变更记录。

许可证信息:显示包的许可证类型,帮助开发者了解如何合法使用包。

导入统计:提供包的导入次数和依赖关系图,帮助开发者了解包的流行程度和关键性。