commit e5190f30098614c09cacd7a361fb1c61264ad971 Author: wangtianqi <1350217033@qq.com> Date: Wed Jun 25 13:26:35 2025 +0800 init diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..537950c --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..f8ee842 --- /dev/null +++ b/Jenkinsfile @@ -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()}" + } + } + } + } +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ed305f0 --- /dev/null +++ b/Makefile @@ -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)" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5290295 --- /dev/null +++ b/README.md @@ -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 + 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)页面报告。 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fc885aa --- /dev/null +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8bb519a --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d615573 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..04aee70 --- /dev/null +++ b/main.go @@ -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() +} \ No newline at end of file diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..d8a3bf6 --- /dev/null +++ b/main_test.go @@ -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") +} \ No newline at end of file