This commit is contained in:
wangtianqi 2025-06-25 13:26:35 +08:00
commit e5190f3009
9 changed files with 1384 additions and 0 deletions

54
Dockerfile Normal file
View File

@ -0,0 +1,54 @@
# 构建阶段
FROM golang:1.21-alpine AS builder
# 设置工作目录
WORKDIR /app
# 安装依赖
RUN apk add --no-cache git ca-certificates tzdata
# 复制go mod文件
COPY go.mod go.sum ./
# 下载依赖
RUN go mod download
# 复制源代码
COPY . .
# 设置构建参数
ARG BUILD_TIME
ARG GIT_COMMIT
# 构建应用
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-w -s -X main.buildTime=${BUILD_TIME} -X main.gitCommit=${GIT_COMMIT}" \
-o main .
# 运行阶段
FROM alpine:latest
# 安装ca证书
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/
# 从builder阶段复制二进制文件
COPY --from=builder /app/main .
# 复制配置文件(可选)
COPY --from=builder /app/.env .
# 创建非root用户
RUN adduser -D -s /bin/sh appuser
USER appuser
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# 运行应用
CMD ["./main"]

501
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,501 @@
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 60, unit: 'MINUTES')
timestamps()
}
environment {
// 目标服务器配置
DEPLOY_SERVER = '116.62.163.84'
// Go相关环境变量
GO_VERSION = '1.21'
CGO_ENABLED = '0'
GOOS = 'linux'
// Docker相关环境变量
IMAGE_NAME = 'golang-demo'
IMAGE_TAG = "${BUILD_NUMBER}"
// SonarQube配置如果需要Go代码扫描
SONAR_HOST_URL = 'http://116.62.163.84:15010'
SONAR_PROJECT_KEY = 'golang-demo'
SONAR_TOKEN = 'squ_7e4217cabd0faae6f3b8ee359b3b8e2ac52eb69a'
}
stages {
stage('Checkout') {
steps {
echo '🔄 开始检出代码...'
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(
script: "git rev-parse --short HEAD",
returnStdout: true
).trim()
env.BUILD_TIME = sh(
script: 'date -u +"%Y-%m-%dT%H:%M:%SZ"',
returnStdout: true
).trim()
}
echo "📋 Git提交ID: ${env.GIT_COMMIT_SHORT}"
echo "⏰ 构建时间: ${env.BUILD_TIME}"
}
}
stage('环境检查') {
steps {
echo '🔍 检查构建环境...'
script {
sh '''
echo "=== Go版本 ==="
go version
echo "=== Go环境信息 ==="
go env
echo "=== Docker版本 ==="
docker --version
echo "=== 工作目录 ==="
pwd && ls -la
echo "=== Go模块信息 ==="
cat go.mod
'''
echo "✅ 构建环境检查完成"
}
}
}
stage('依赖管理') {
steps {
echo '📦 下载Go依赖...'
sh '''
echo "下载依赖..."
go mod download
echo "验证依赖..."
go mod verify
echo "整理依赖..."
go mod tidy
echo "✅ 依赖管理完成"
'''
}
}
stage('代码检查') {
steps {
echo '🔍 运行Go代码检查...'
sh '''
echo "运行go vet..."
go vet ./...
echo "运行go fmt检查..."
if [ "$(gofmt -l . | wc -l)" -gt 0 ]; then
echo "❌ 代码格式不正确,需要运行 go fmt"
gofmt -l .
exit 1
fi
echo "✅ 代码检查通过"
'''
}
}
stage('单元测试') {
steps {
echo '🧪 运行单元测试...'
sh '''
echo "运行测试并生成覆盖率报告..."
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
echo "生成HTML覆盖率报告..."
go tool cover -html=coverage.out -o coverage.html
echo "显示覆盖率统计..."
go tool cover -func=coverage.out
'''
}
post {
always {
script {
// 发布覆盖率报告
if (fileExists('coverage.html')) {
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: 'coverage.html',
reportName: 'Go Coverage Report'
])
echo '✅ Go覆盖率报告已发布'
}
// 归档覆盖率文件
if (fileExists('coverage.out')) {
archiveArtifacts artifacts: 'coverage.out', fingerprint: true
}
}
}
}
}
stage('代码质量扫描') {
steps {
echo '🔍 运行SonarQube代码扫描...'
script {
try {
// 使用sonar-scanner for Go项目
sh """
# 创建sonar-project.properties文件
cat > sonar-project.properties << EOF
sonar.projectKey=${SONAR_PROJECT_KEY}
sonar.projectName=golang-demo
sonar.projectVersion=1.0.0
sonar.sources=.
sonar.exclusions=vendor/**,**/*_test.go,**/testdata/**
sonar.tests=.
sonar.test.inclusions=**/*_test.go
sonar.go.coverage.reportPaths=coverage.out
sonar.host.url=${SONAR_HOST_URL}
sonar.login=${SONAR_TOKEN}
EOF
# 运行sonar-scanner
sonar-scanner || echo "SonarQube扫描工具未安装跳过扫描"
"""
echo "✅ SonarQube代码扫描完成"
} catch (Exception e) {
echo "⚠️ SonarQube扫描失败继续构建流程: ${e.getMessage()}"
}
}
}
}
stage('编译构建') {
steps {
echo '🔨 编译Go应用程序...'
sh '''
echo "开始编译..."
CGO_ENABLED=0 GOOS=linux go build \\
-ldflags="-w -s -X main.buildTime=${BUILD_TIME} -X main.gitCommit=${GIT_COMMIT_SHORT}" \\
-o golang-demo .
echo "验证二进制文件..."
file golang-demo
ls -lh golang-demo
echo "✅ 编译完成"
'''
}
post {
success {
script {
if (fileExists('golang-demo')) {
archiveArtifacts artifacts: 'golang-demo', fingerprint: true
}
}
}
}
}
stage('构建Docker镜像') {
steps {
echo '🐳 构建Docker镜像...'
script {
try {
// 清理旧镜像
sh 'docker image prune -f || true'
echo "开始构建Docker镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
// 构建Docker镜像
timeout(time: 20, unit: 'MINUTES') {
sh """
# 构建Docker镜像传入构建参数
docker build \\
--build-arg BUILD_TIME="${BUILD_TIME}" \\
--build-arg GIT_COMMIT="${GIT_COMMIT_SHORT}" \\
-t ${IMAGE_NAME}:${IMAGE_TAG} \\
-t ${IMAGE_NAME}:latest .
echo "✅ Docker镜像构建完成"
# 验证镜像
docker images ${IMAGE_NAME}:${IMAGE_TAG}
"""
}
} catch (Exception e) {
echo "❌ Docker构建失败: ${e.getMessage()}"
// 显示更多调试信息
sh '''
echo "=== Docker系统信息 ==="
docker system info
echo "=== Docker磁盘使用情况 ==="
docker system df
echo "=== 当前镜像列表 ==="
docker images
'''
throw e
}
}
}
}
stage('镜像测试') {
steps {
echo '🧪 测试Docker镜像...'
script {
try {
sh """
# 启动测试容器
docker run -d --name test-${BUILD_NUMBER} -p 8081:8080 ${IMAGE_NAME}:${IMAGE_TAG}
# 等待容器启动
sleep 10
# 测试健康检查
curl -f http://localhost:8081/health || exit 1
curl -f http://localhost:8081/ping || exit 1
curl -f http://localhost:8081/version || exit 1
echo "✅ 镜像测试通过"
"""
} finally {
// 清理测试容器
sh """
docker stop test-${BUILD_NUMBER} || true
docker rm test-${BUILD_NUMBER} || true
"""
}
}
}
}
stage('部署应用') {
steps {
echo '🚀 部署应用到服务器...'
script {
// 根据分支决定部署端口和配置
def deployPort = '15021'
def containerName = 'golang-demo'
def environment = 'production'
if (env.BRANCH_NAME == 'develop' || env.BRANCH_NAME?.startsWith('feature/')) {
deployPort = '15022' // 测试环境使用不同端口
containerName = 'golang-demo-test'
environment = 'test'
}
// 传输镜像并部署
sshagent(['deploy-server-ssh-key']) {
sh """
# 保存Docker镜像为tar文件
echo "📤 保存镜像为文件..."
docker save ${IMAGE_NAME}:${IMAGE_TAG} -o ${IMAGE_NAME}-${IMAGE_TAG}.tar
# 传输镜像文件到目标服务器
echo "📤 传输镜像到服务器..."
scp -o StrictHostKeyChecking=no ${IMAGE_NAME}-${IMAGE_TAG}.tar root@${DEPLOY_SERVER}:/tmp/
# 在目标服务器上部署
ssh -o StrictHostKeyChecking=no root@${DEPLOY_SERVER} << 'EOF'
echo "🔄 在服务器上部署应用..."
# 加载Docker镜像
docker load -i /tmp/${IMAGE_NAME}-${IMAGE_TAG}.tar
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
# 停止并删除现有容器
docker stop ${containerName} || true
docker rm ${containerName} || true
# 运行新容器
docker run -d --name ${containerName} \\
-p ${deployPort}:8080 \\
--restart unless-stopped \\
-e GIN_MODE=release \\
-e ENVIRONMENT=${environment} \\
${IMAGE_NAME}:${IMAGE_TAG}
# 清理临时文件
rm -f /tmp/${IMAGE_NAME}-${IMAGE_TAG}.tar
echo "✅ 应用部署完成,端口: ${deployPort}"
echo "🔗 访问地址: http://${DEPLOY_SERVER}:${deployPort}"
EOF
# 清理本地临时文件
rm -f ${IMAGE_NAME}-${IMAGE_TAG}.tar
"""
}
echo "✅ 应用部署完成!"
}
}
}
stage('健康检查') {
steps {
echo '🏥 执行应用健康检查...'
script {
// 等待应用启动
sleep(time: 30, unit: 'SECONDS')
def deployPort = '15020'
if (env.BRANCH_NAME == 'develop' || env.BRANCH_NAME?.startsWith('feature/')) {
deployPort = '15021'
}
try {
// Go应用的健康检查端点
def healthCheckUrl = "http://${DEPLOY_SERVER}:${deployPort}/health"
def response = sh(
script: "curl -s -o /dev/null -w '%{http_code}' ${healthCheckUrl} || echo '000'",
returnStdout: true
).trim()
if (response == "200") {
echo "✅ 应用健康检查通过"
// 额外检查其他端点
def pingResponse = sh(
script: "curl -s -o /dev/null -w '%{http_code}' http://${DEPLOY_SERVER}:${deployPort}/ping || echo '000'",
returnStdout: true
).trim()
if (pingResponse == "200") {
echo "✅ Ping端点检查通过"
}
} else {
echo "⚠️ 应用健康检查失败HTTP状态码: ${response}"
echo "🔄 应用可能还在启动中..."
// 再等待一段时间重试
sleep(time: 30, unit: 'SECONDS')
response = sh(
script: "curl -s -o /dev/null -w '%{http_code}' ${healthCheckUrl} || echo '000'",
returnStdout: true
).trim()
if (response == "200") {
echo "✅ 重试后健康检查通过"
} else {
echo "⚠️ 健康检查仍未通过,但不阻止构建"
}
}
} catch (Exception e) {
echo "⚠️ 健康检查异常: ${e.getMessage()}"
}
}
}
}
}
post {
always {
script {
echo '🧹 清理工作空间...'
try {
// 清理Go构建产物
sh '''
rm -f golang-demo
rm -f coverage.out coverage.html
rm -f sonar-project.properties
'''
// 清理Docker资源
sh '''
# 清理未使用的镜像
docker image prune -f || true
# 清理构建缓存
docker builder prune -f || true
'''
} catch (Exception e) {
echo "⚠️ 清理失败: ${e.getMessage()}"
}
}
}
success {
script {
echo '✅ 流水线执行成功!'
def deployPort = '15020'
if (env.BRANCH_NAME == 'develop' || env.BRANCH_NAME?.startsWith('feature/')) {
deployPort = '15021'
}
def message = """
🎉 Jenkins构建成功
📋 项目: ${env.JOB_NAME}
🔢 构建号: ${env.BUILD_NUMBER}
🌿 分支: ${env.BRANCH_NAME ?: 'unknown'}
📝 提交: ${env.GIT_COMMIT_SHORT ?: 'unknown'}
⏰ 构建时间: ${env.BUILD_TIME ?: 'unknown'}
⏱️ 持续时间: ${currentBuild.durationString}
🔗 构建链接: ${env.BUILD_URL}
🌐 应用地址: http://${DEPLOY_SERVER}:${deployPort}
🏥 健康检查: http://${DEPLOY_SERVER}:${deployPort}/health
🏓 Ping测试: http://${DEPLOY_SERVER}:${deployPort}/ping
"""
echo message
}
}
failure {
script {
echo '❌ 流水线执行失败!'
def message = """
💥 Jenkins构建失败
📋 项目: ${env.JOB_NAME}
🔢 构建号: ${env.BUILD_NUMBER}
🌿 分支: ${env.BRANCH_NAME ?: 'unknown'}
📝 提交: ${env.GIT_COMMIT_SHORT ?: 'unknown'}
⏰ 构建时间: ${env.BUILD_TIME ?: 'unknown'}
⏱️ 持续时间: ${currentBuild.durationString}
🔗 构建链接: ${env.BUILD_URL}
📄 查看日志: ${env.BUILD_URL}console
"""
echo message
// 清理可能的测试容器
sh '''
docker stop test-${BUILD_NUMBER} || true
docker rm test-${BUILD_NUMBER} || true
'''
}
}
cleanup {
script {
try {
// 清理工作空间
cleanWs()
echo "✅ 清理完成"
} catch (Exception e) {
echo "⚠️ 清理失败: ${e.getMessage()}"
}
}
}
}
}

129
Makefile Normal file
View File

@ -0,0 +1,129 @@
# 应用配置
APP_NAME := golang-demo
VERSION := 1.0.0
BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
GIT_COMMIT := $(shell git rev-parse --short HEAD)
# Go配置
GOCMD := go
GOBUILD := $(GOCMD) build
GOCLEAN := $(GOCMD) clean
GOTEST := $(GOCMD) test
GOGET := $(GOCMD) get
GOMOD := $(GOCMD) mod
# Docker配置
DOCKER_IMAGE := $(APP_NAME):$(VERSION)
DOCKER_LATEST := $(APP_NAME):latest
# 构建标志
LDFLAGS := -ldflags="-w -s -X main.buildTime=$(BUILD_TIME) -X main.gitCommit=$(GIT_COMMIT)"
.PHONY: help build test clean run deps docker-build docker-run docker-stop docker-clean all
# 默认目标
all: deps test build
# 显示帮助信息
help:
@echo "Available commands:"
@echo " deps - Download dependencies"
@echo " test - Run tests"
@echo " build - Build the application"
@echo " run - Run the application"
@echo " clean - Clean build artifacts"
@echo " docker-build - Build Docker image"
@echo " docker-run - Run Docker container"
@echo " docker-stop - Stop Docker container"
@echo " docker-clean - Clean Docker images"
@echo " dev - Run in development mode"
@echo " prod - Run in production mode"
# 下载依赖
deps:
@echo "Downloading dependencies..."
$(GOMOD) download
$(GOMOD) verify
$(GOMOD) tidy
# 运行测试
test:
@echo "Running tests..."
$(GOTEST) -v ./...
$(GOCMD) vet ./...
# 构建应用
build:
@echo "Building $(APP_NAME)..."
CGO_ENABLED=0 GOOS=linux $(GOBUILD) $(LDFLAGS) -o $(APP_NAME) .
# 运行应用
run: build
@echo "Running $(APP_NAME)..."
./$(APP_NAME)
# 开发模式运行
dev:
@echo "Running in development mode..."
GIN_MODE=debug $(GOCMD) run .
# 生产模式运行
prod:
@echo "Running in production mode..."
GIN_MODE=release ./$(APP_NAME)
# 清理构建产物
clean:
@echo "Cleaning..."
$(GOCLEAN)
rm -f $(APP_NAME)
# 构建Docker镜像
docker-build:
@echo "Building Docker image..."
docker build \
--build-arg BUILD_TIME="$(BUILD_TIME)" \
--build-arg GIT_COMMIT="$(GIT_COMMIT)" \
-t $(DOCKER_IMAGE) \
-t $(DOCKER_LATEST) .
# 运行Docker容器
docker-run: docker-build
@echo "Running Docker container..."
docker run -d \
--name $(APP_NAME) \
-p 8080:8080 \
-e GIN_MODE=release \
$(DOCKER_LATEST)
# 停止Docker容器
docker-stop:
@echo "Stopping Docker container..."
docker stop $(APP_NAME) || true
docker rm $(APP_NAME) || true
# 清理Docker镜像
docker-clean:
@echo "Cleaning Docker images..."
docker rmi $(DOCKER_IMAGE) || true
docker rmi $(DOCKER_LATEST) || true
docker image prune -f
# 重新部署
redeploy: docker-stop docker-run
# 查看日志
logs:
docker logs -f $(APP_NAME)
# 健康检查
health:
@echo "Checking application health..."
curl -f http://localhost:8080/health || echo "Health check failed"
# 显示版本信息
version:
@echo "App: $(APP_NAME)"
@echo "Version: $(VERSION)"
@echo "Build Time: $(BUILD_TIME)"
@echo "Git Commit: $(GIT_COMMIT)"

240
README.md Normal file
View File

@ -0,0 +1,240 @@
# Golang Demo - 现代化Web API项目
这是一个基于Gin框架的现代化Go web项目专为Jenkins CI/CD部署流程设计。
## 🚀 特性
- **现代Go技术栈**: 使用Go 1.21+和最新的依赖包
- **Gin Web框架**: 高性能的HTTP web框架
- **结构化日志**: 使用Zap进行高性能日志记录
- **优雅关闭**: 支持优雅的服务器关闭
- **健康检查**: 内置健康检查端点,适合容器化部署
- **Docker支持**: 多阶段构建,优化镜像大小
- **CI/CD就绪**: 包含完整的Jenkins Pipeline配置
## 📋 API端点
### 基础端点
- `GET /` - 欢迎页面
- `GET /ping` - 简单的ping测试
- `GET /health` - 健康检查(适合负载均衡器)
- `GET /version` - 版本信息
### API端点 (v1)
- `GET /api/v1/status` - API状态信息
- `GET /api/v1/time` - 当前时间信息
- `POST /api/v1/echo` - 回显请求body
## 🛠️ 技术栈
- **语言**: Go 1.21+
- **Web框架**: Gin v1.9.1
- **日志**: Zap v1.26.0
- **配置**: godotenv v1.5.1
- **容器**: Docker & Docker Compose
- **CI/CD**: Jenkins Pipeline
## 🚀 快速开始
### 本地开发
1. **克隆项目**
```bash
git clone <repository-url>
cd golang_demo
```
2. **安装依赖**
```bash
make deps
```
3. **运行测试**
```bash
make test
```
4. **开发模式运行**
```bash
make dev
```
5. **访问应用**
```
http://localhost:8080
```
### 使用Docker
1. **构建并运行**
```bash
make docker-run
```
2. **查看日志**
```bash
make logs
```
3. **停止容器**
```bash
make docker-stop
```
### 使用Docker Compose
```bash
docker-compose up -d
```
## 🔧 配置
项目使用环境变量进行配置,可以通过`.env`文件设置:
```env
PORT=8080
GIN_MODE=debug
```
### 环境变量说明
- `PORT`: 服务端口默认8080
- `GIN_MODE`: Gin运行模式debug/release/test
## 📦 构建
### 本地构建
```bash
make build
```
### Docker构建
```bash
make docker-build
```
构建时会注入以下信息:
- 构建时间
- Git提交哈希
- 版本信息
## 🚀 部署
### Jenkins Pipeline
项目包含完整的`Jenkinsfile`,支持:
- **代码检出** - 自动获取Git提交信息和构建时间
- **环境检查** - 验证Go、Docker等构建环境
- **依赖管理** - 自动下载和验证Go模块依赖
- **代码检查** - 运行`go vet``go fmt`检查
- **单元测试** - 执行测试并生成覆盖率报告
- **代码质量扫描** - SonarQube代码质量分析可选
- **编译构建** - 交叉编译Linux二进制文件
- **Docker镜像构建** - 多阶段构建优化镜像
- **镜像测试** - 验证Docker镜像功能正常
- **自动部署** - 支持多环境部署(生产/测试)
- **健康检查** - 验证部署后应用状态
- **构建产物清理** - 自动清理临时文件和镜像
**分支策略:**
- `main`/`master`分支部署到生产环境端口15020
- `develop`/`feature/*`分支部署到测试环境端口15021
### 手动部署
1. **构建镜像**
```bash
docker build -t golang-demo:latest .
```
2. **运行容器**
```bash
docker run -d \
--name golang-demo \
-p 8080:8080 \
-e GIN_MODE=release \
golang-demo:latest
```
3. **健康检查**
```bash
curl http://localhost:8080/health
```
## 🧪 测试
### 运行所有测试
```bash
make test
```
### 健康检查测试
```bash
make health
```
### API测试示例
```bash
# 基础测试
curl http://localhost:8080/ping
# 健康检查
curl http://localhost:8080/health
# API测试
curl http://localhost:8080/api/v1/time
# POST请求测试
curl -X POST http://localhost:8080/api/v1/echo \
-H "Content-Type: application/json" \
-d '{"message": "Hello World"}'
```
## 📖 开发指南
### 项目结构
```
golang_demo/
├── main.go # 主程序文件
├── go.mod # Go模块文件
├── go.sum # 依赖锁定文件
├── .env # 环境变量配置
├── Dockerfile # Docker构建文件
├── docker-compose.yml # Docker Compose配置
├── Jenkinsfile # Jenkins Pipeline配置
├── Makefile # 构建脚本
└── README.md # 项目说明
```
### 添加新的API端点
1. 在`setupRouter()`函数中添加路由
2. 实现对应的处理函数
3. 更新API文档
### 自定义配置
修改`.env`文件或设置环境变量来自定义配置。
## 🤝 贡献
1. Fork本项目
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启Pull Request
## 📝 许可证
本项目采用MIT许可证 - 查看[LICENSE](LICENSE)文件了解详情。
## 🐛 问题报告
如果发现bug或有功能建议请在[Issues](../../issues)页面报告。

47
docker-compose.yml Normal file
View File

@ -0,0 +1,47 @@
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
BUILD_TIME: ${BUILD_TIME:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")}
GIT_COMMIT: ${GIT_COMMIT:-unknown}
ports:
- "8080:8080"
environment:
- GIN_MODE=release
- PORT=8080
volumes:
- ./.env:/root/.env
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# 可选:添加数据库服务
# postgres:
# image: postgres:15-alpine
# environment:
# POSTGRES_DB: golang_demo
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: password
# ports:
# - "5432:5432"
# volumes:
# - postgres_data:/var/lib/postgresql/data
# restart: unless-stopped
# 可选添加Redis服务
# redis:
# image: redis:7-alpine
# ports:
# - "6379:6379"
# restart: unless-stopped
# volumes:
# postgres_data:

40
go.mod Normal file
View File

@ -0,0 +1,40 @@
module golang_demo
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.8.3
go.uber.org/zap v1.26.0
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

94
go.sum Normal file
View File

@ -0,0 +1,94 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

195
main.go Normal file
View File

@ -0,0 +1,195 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"go.uber.org/zap"
)
var logger *zap.Logger
func main() {
// 初始化日志
var err error
logger, err = zap.NewProduction()
if err != nil {
panic(fmt.Sprintf("Failed to initialize logger: %v", err))
}
defer logger.Sync()
// 加载环境变量
if err := godotenv.Load(); err != nil {
logger.Warn("No .env file found, using system environment variables")
}
// 设置Gin模式
mode := getEnv("GIN_MODE", "debug")
gin.SetMode(mode)
// 创建路由
router := setupRouter()
// 获取端口
port := getEnv("PORT", "8080")
// 创建HTTP服务器
srv := &http.Server{
Addr: ":" + port,
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 在单独的goroutine中启动服务器
go func() {
logger.Info("Starting server", zap.String("port", port), zap.String("mode", mode))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("Failed to start server", zap.Error(err))
}
}()
// 等待中断信号来优雅地关闭服务器
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("Shutting down server...")
// 给服务器5秒钟来完成正在处理的请求
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Fatal("Server forced to shutdown", zap.Error(err))
}
logger.Info("Server exited")
}
func setupRouter() *gin.Engine {
router := gin.New()
// 使用自定义中间件
router.Use(ginZapLogger(logger), ginZapRecovery(logger, true))
// 基本路由
router.GET("/", handleHome)
router.GET("/ping", handlePing)
router.GET("/health", handleHealth)
router.GET("/version", handleVersion)
// API组
api := router.Group("/api/v1")
{
api.GET("/status", handleAPIStatus)
api.GET("/time", handleTime)
api.POST("/echo", handleEcho)
}
return router
}
// 处理函数
func handleHome(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Welcome to Golang Demo API",
"version": "1.0.0",
"environment": getEnv("GIN_MODE", "development"),
"timestamp": time.Now().Format(time.RFC3339),
})
}
func handlePing(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
"time": time.Now().Unix(),
})
}
func handleHealth(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"timestamp": time.Now().Format(time.RFC3339),
"uptime": time.Since(startTime).String(),
})
}
func handleVersion(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"version": "1.0.0",
"build_time": buildTime,
"go_version": "1.21+",
"git_commit": gitCommit,
})
}
func handleAPIStatus(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"api_version": "v1",
"status": "active",
"endpoints": []string{
"GET /api/v1/status",
"GET /api/v1/time",
"POST /api/v1/echo",
},
})
}
func handleTime(c *gin.Context) {
now := time.Now()
c.JSON(http.StatusOK, gin.H{
"timestamp": now.Unix(),
"iso": now.Format(time.RFC3339),
"utc": now.UTC().Format(time.RFC3339),
"timezone": now.Location().String(),
})
}
func handleEcho(c *gin.Context) {
var body map[string]interface{}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid JSON",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"echo": body,
"timestamp": time.Now().Format(time.RFC3339),
"client_ip": c.ClientIP(),
})
}
// 工具函数
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// 构建时注入的变量
var (
startTime = time.Now()
buildTime = "unknown"
gitCommit = "unknown"
)
// Gin中间件
func ginZapLogger(logger *zap.Logger) gin.HandlerFunc {
return gin.LoggerWithWriter(gin.DefaultWriter)
}
func ginZapRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
return gin.Recovery()
}

84
main_test.go Normal file
View File

@ -0,0 +1,84 @@
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
gin.SetMode(gin.TestMode)
m.Run()
}
func TestPingRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "pong")
}
func TestHealthRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/health", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "healthy")
}
func TestHomeRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Welcome to Golang Demo API")
}
func TestVersionRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/version", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "version")
}
func TestAPIStatusRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/status", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "v1")
}
func TestEchoRoute(t *testing.T) {
router := setupRouter()
body := `{"message": "test message"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/echo", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "test message")
}