1
0

feat: 配置 golangci-lint 静态分析并修复存量违规

- 新增 backend/.golangci.yml 配置 12 个 linter(forbidigo、errorlint、errcheck、staticcheck、revive、gocritic、gosec、bodyclose、noctx、nilerr、goimports、gocyclo)
- 新增 lefthook.yml 配置 pre-commit hook 自动运行 lint
- 修复存量代码违规:errors.Is/As 替换、zap.Error 替换、import 排序、errcheck 修复
- 更新 README 补充编码规范说明
- 归档 backend-code-lint 变更
This commit is contained in:
2026-04-24 13:01:48 +08:00
parent 4c78ab6cc8
commit 4c6b49099d
96 changed files with 1290 additions and 1348 deletions

View File

@@ -6,19 +6,25 @@ import (
"fmt"
"os/exec"
"strings"
"go.uber.org/zap"
)
func showError(title, message string) {
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "%s"`,
escapeAppleScript(message), escapeAppleScript(title))
exec.Command("osascript", "-e", script).Run()
if err := exec.Command("osascript", "-e", script).Run(); err != nil {
dialogLogger().Warn("显示错误对话框失败", zap.Error(err))
}
}
func showAbout() {
message := "Nex Gateway\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway"
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "关于 Nex Gateway"`,
escapeAppleScript(message))
exec.Command("osascript", "-e", script).Run()
if err := exec.Command("osascript", "-e", script).Run(); err != nil {
dialogLogger().Warn("显示关于对话框失败", zap.Error(err))
}
}
func escapeAppleScript(s string) string {

View File

@@ -4,7 +4,6 @@ package main
import (
"fmt"
"os"
"os/exec"
"sync"
)
@@ -63,7 +62,7 @@ func showError(title, message string) {
exec.Command("xmessage", "-center",
fmt.Sprintf("%s: %s", title, message)).Run()
default:
fmt.Fprintf(os.Stderr, "错误: %s: %s\n", title, message)
dialogLogger().Error("无法显示错误对话框")
}
}
@@ -83,6 +82,6 @@ func showAbout() {
exec.Command("xmessage", "-center",
fmt.Sprintf("关于 Nex Gateway: %s", message)).Run()
default:
fmt.Fprintf(os.Stderr, "关于 Nex Gateway: %s\n", message)
dialogLogger().Info("关于 Nex Gateway")
}
}

View File

@@ -0,0 +1,15 @@
package main
import (
"go.uber.org/zap"
pkgLogger "nex/backend/pkg/logger"
)
func dialogLogger() *zap.Logger {
if zapLogger != nil {
return zapLogger
}
return pkgLogger.NewMinimal()
}

View File

@@ -13,10 +13,7 @@ import (
"strings"
"time"
"github.com/getlantern/systray"
"github.com/gin-gonic/gin"
"github.com/gofrs/flock"
"go.uber.org/zap"
"nex/embedfs"
"nex/backend/internal/config"
"nex/backend/internal/conversion"
@@ -28,9 +25,13 @@ import (
"nex/backend/internal/provider"
"nex/backend/internal/repository"
"nex/backend/internal/service"
pkgLogger "nex/backend/pkg/logger"
"nex/embedfs"
"github.com/getlantern/systray"
"github.com/gin-gonic/gin"
"github.com/gofrs/flock"
"go.uber.org/zap"
pkgLogger "nex/backend/pkg/logger"
)
var (
@@ -51,12 +52,16 @@ func main() {
showError("Nex Gateway", "已有 Nex 实例运行")
os.Exit(1)
}
defer singleLock.Unlock()
defer func() {
if err := singleLock.Unlock(); err != nil {
minimalLogger.Warn("释放实例锁失败", zap.Error(err))
}
}()
if err := checkPortAvailable(port); err != nil {
minimalLogger.Error("端口不可用", zap.Error(err))
showError("Nex Gateway", err.Error())
os.Exit(1)
return
}
cfg, err := config.LoadConfig()
@@ -75,7 +80,11 @@ func main() {
if err != nil {
minimalLogger.Fatal("初始化日志失败", zap.Error(err))
}
defer zapLogger.Sync()
defer func() {
if err := zapLogger.Sync(); err != nil {
minimalLogger.Warn("同步日志失败", zap.Error(err))
}
}()
cfg.PrintSummary(zapLogger)
@@ -144,14 +153,14 @@ func main() {
go func() {
zapLogger.Info("AI Gateway 启动", zap.String("addr", server.Addr))
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
zapLogger.Fatal("服务器启动失败", zap.String("error", err.Error()))
zapLogger.Fatal("服务器启动失败", zap.Error(err))
}
}()
go func() {
time.Sleep(500 * time.Millisecond)
if err := openBrowser(fmt.Sprintf("http://localhost:%d", port)); err != nil {
zapLogger.Warn("无法打开浏览器", zap.String("error", err.Error()))
zapLogger.Warn("无法打开浏览器", zap.Error(err))
}
}()
@@ -193,7 +202,7 @@ func setupRoutes(r *gin.Engine, proxyHandler *handler.ProxyHandler, providerHand
func setupStaticFiles(r *gin.Engine) {
distFS, err := fs.Sub(embedfs.FrontendDist, "frontend-dist")
if err != nil {
zapLogger.Fatal("无法加载前端资源", zap.String("error", err.Error()))
zapLogger.Fatal("无法加载前端资源", zap.Error(err))
}
getContentType := func(path string) string {
@@ -266,7 +275,7 @@ func setupSystray(port int) {
icon, err = embedfs.Assets.ReadFile("assets/icon.png")
}
if err != nil {
zapLogger.Error("无法加载托盘图标", zap.String("error", err.Error()))
zapLogger.Error("无法加载托盘图标", zap.Error(err))
}
systray.SetIcon(icon)
systray.SetTitle("Nex Gateway")
@@ -287,7 +296,9 @@ func setupSystray(port int) {
for {
select {
case <-mOpen.ClickedCh:
openBrowser(fmt.Sprintf("http://localhost:%d", port))
if err := openBrowser(fmt.Sprintf("http://localhost:%d", port)); err != nil {
zapLogger.Warn("打开浏览器失败", zap.Error(err))
}
case <-mAbout.ClickedCh:
showAbout()
case <-mQuit.ClickedCh:
@@ -308,7 +319,9 @@ func doShutdown() {
if server != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx)
if err := server.Shutdown(ctx); err != nil && zapLogger != nil {
zapLogger.Warn("关闭服务器失败", zap.Error(err))
}
}
if shutdownCancel != nil {
@@ -346,8 +359,8 @@ func (s *SingletonLock) Lock() error {
return nil
}
func (s *SingletonLock) Unlock() {
s.flock.Unlock()
func (s *SingletonLock) Unlock() error {
return s.flock.Unlock()
}
func openBrowser(url string) error {

View File

@@ -21,7 +21,7 @@ func TestCheckPortAvailable(t *testing.T) {
func TestCheckPortOccupied(t *testing.T) {
port := 19827
listener, err := net.Listen("tcp", ":19827")
listener, err := net.Listen("tcp", "127.0.0.1:19827")
if err != nil {
t.Fatalf("无法启动测试服务器: %v", err)
}
@@ -47,13 +47,19 @@ func TestCheckPortOccupied(t *testing.T) {
func TestCheckPortAvailableAfterClose(t *testing.T) {
port := 19828
listener, err := net.Listen("tcp", ":19828")
listener, err := net.Listen("tcp", "127.0.0.1:19828")
if err != nil {
t.Fatalf("无法启动测试服务器: %v", err)
}
server := &http.Server{}
go server.Serve(listener)
server := &http.Server{ReadHeaderTimeout: time.Second}
defer server.Close()
go func() {
err := server.Serve(listener)
if err != nil && err != http.ErrServerClosed {
t.Errorf("serve failed: %v", err)
}
}()
time.Sleep(100 * time.Millisecond)

View File

@@ -14,7 +14,11 @@ func TestSingletonLock_FirstLockSuccess(t *testing.T) {
if err := lock.Lock(); err != nil {
t.Fatalf("首次加锁应成功,但返回错误: %v", err)
}
defer lock.Unlock()
defer func() {
if err := lock.Unlock(); err != nil {
t.Fatalf("解锁失败: %v", err)
}
}()
}
func TestSingletonLock_DuplicateLockFails(t *testing.T) {
@@ -25,12 +29,18 @@ func TestSingletonLock_DuplicateLockFails(t *testing.T) {
if err := lock1.Lock(); err != nil {
t.Fatalf("首次加锁应成功: %v", err)
}
defer lock1.Unlock()
defer func() {
if err := lock1.Unlock(); err != nil {
t.Fatalf("解锁失败: %v", err)
}
}()
lock2 := NewSingletonLock(lockPath)
err := lock2.Lock()
if err == nil {
lock2.Unlock()
if unlockErr := lock2.Unlock(); unlockErr != nil {
t.Fatalf("解锁失败: %v", unlockErr)
}
t.Fatal("重复加锁应失败,但返回 nil")
}
}
@@ -43,16 +53,22 @@ func TestSingletonLock_UnlockThenRelock(t *testing.T) {
if err := lock1.Lock(); err != nil {
t.Fatalf("首次加锁应成功: %v", err)
}
lock1.Unlock()
if err := lock1.Unlock(); err != nil {
t.Fatalf("解锁失败: %v", err)
}
lock2 := NewSingletonLock(lockPath)
if err := lock2.Lock(); err != nil {
t.Fatalf("释放后重新加锁应成功: %v", err)
}
lock2.Unlock()
if err := lock2.Unlock(); err != nil {
t.Fatalf("解锁失败: %v", err)
}
}
func TestSingletonLock_UnlockWithoutLock(t *testing.T) {
lock := NewSingletonLock(filepath.Join(os.TempDir(), "nex-gateway-test-nil.lock"))
lock.Unlock()
if err := lock.Unlock(); err != nil {
t.Fatalf("未加锁时解锁失败: %v", err)
}
}

View File

@@ -6,9 +6,9 @@ import (
"strings"
"testing"
"github.com/gin-gonic/gin"
"nex/embedfs"
"github.com/gin-gonic/gin"
)
func TestSetupStaticFiles(t *testing.T) {

View File

@@ -44,7 +44,11 @@ func main() {
if err != nil {
minimalLogger.Fatal("初始化日志失败", zap.Error(err))
}
defer zapLogger.Sync()
defer func() {
if err := zapLogger.Sync(); err != nil {
minimalLogger.Warn("同步日志失败", zap.Error(err))
}
}()
cfg.PrintSummary(zapLogger)