init
This commit is contained in:
commit
e5190f3009
54
Dockerfile
Normal file
54
Dockerfile
Normal 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
501
Jenkinsfile
vendored
Normal 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
129
Makefile
Normal 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
240
README.md
Normal 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
47
docker-compose.yml
Normal 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
40
go.mod
Normal 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
94
go.sum
Normal 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
195
main.go
Normal 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
84
main_test.go
Normal 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")
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user