go语言web单体项目模板搭建

代码仓库

gitee本例代码
gitee之前写的

创建项目

先创建目录, 然后在该目录下执行

go mod init quick-start
# 每次运行前执行, 确保项目依赖关系正确
go mod tidy
# 运行
go run main.go

目录结构

一般web工程采用MVC结构, M就是model, V是view, C是controller

  • Model(模型)
    负责管理应用程序的核心数据和业务逻辑。
    与数据库或其他数据源交互,并处理数据的增删改查操作。
    当数据发生变化时,通知View进行更新。
  • View(视图)
    负责显示用户界面,通常基于Model的数据进行渲染。
    用户可以通过View与应用程序进行交互。
    View监听Model的变化并自动刷新显示。
  • Controller(控制器)
    负责接收用户的输入(例如点击按钮、提交表单等),并协调Model和View之间的交互。
    根据用户的操作调用Model的方法或更新View的状态。
    控制器充当Model和View之间的桥梁。

所以一般的目录结构包含
models/controllers/middleware
其中, models存放的是模型文件, controllers存放的是控制器文件, middleware存放的是中间件文件
而views则一般是单独分离出前端项目来开发

- common/ 或 utils/
- controllers/
- initializers/
- middleware/
- migrate/
- models/
- scripts/
- .env
- go.mod
- go.sum
- main.go
- readme.md

数据库

我们使用gorm框架操作数据库

创建数据库表

gorm支持自动生成数据库表结构, 只需要定义模型结构体, 然后使用gorm的代码来生成就行

先连接到数据库, 为了方便, 我们使用sqlite

// initializers/loadEnv.go
/*
 * @Author Malred
 * @Date 2025-06-15 21:24:25
 * @Description 连接到数据库, 对外提供数据库实例
 */
package initializers

import (
	"log"
	"os"

	"gorm.io/driver/mysql"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

var DB *gorm.DB

func ConnectToDB() {
	//ConnectToMysql()
	ConnectToSqlite()
}

func ConnectToMysql() {
	var err error
	dsn := os.Getenv("MYSQL_DB_URL")
	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal("Failed to connect to database")
	}
}

func ConnectToSqlite() {
	var err error
	dbPath := os.Getenv("SQLITE_MDB_URL") // 通过环境变量指定 SQLite 数据库文件路径
	DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
	if err != nil {
		log.Fatal("Failed to connect to database")
	}
}

数据库等配置, 我们使用.env文件来保存

# web运行端口
PORT=4000
# 随机生成的UUID, 用于生成JWT
SECRET=8d6f7b7d-2f24-4867-ae90-feb4fc646693
# MYSQL数据库连接地址
MYSQL_DB_URL=root:123456@tcp(127.0.0.1:3306)/quick-starter?charset=utf8mb4&parseTime=True&loc=Local
# SQLITE数据库连接地址
SQLITE_MDB_URL=./dev.db

go语言加载.env文件

// initializers/loadEnv.go
/*
 * @Author Malred
 * @Date 2025-06-15 21:24:15
 * @Description 加载.env文件中的环境变量
 */
package initializers

import (
	"github.com/joho/godotenv"
	"log"
)

func LoadEnvVariables() {
	// 加载.env, 里面设置了port, gin会读取并设置端口号
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}
}

声明对应数据库表的模型model

// models/user.go
/*
 * @Author Malred
 * @Date 2025-06-15 21:35:54
 * @Description
 */
package models

type User struct {
	ID       uint `gorm:"primarykey"`
	Username string
	Password string
	Email    string
}
// models/post.go
/*
 * @Author Malred
 * @Date 2025-06-15 21:36:06
 * @Description
 */
package models

import "gorm.io/gorm"

type Post struct {
	gorm.Model
	Title string
	Body  string
}

用代码生成数据库表

// migrate/migrate.go
/*
 * @Author Malred
 * @Date 2025-06-15 21:22:42
 * @Description 使用gorm框架的方法, 创建数据库表
 */
package main

import (
	"quick-start/initializers"
	"quick-start/models"
)

func init() {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()
}

func main() {
	// 创建数据库
	initializers.DB.AutoMigrate(&models.User{})
	initializers.DB.AutoMigrate(&models.Post{})
}

用户信息表

增删改查

实现对用户信息的基本操作:增加、删除、修改和查询。

// user_test.go
/*
 * @Author Malred
 * @Date 2025-06-15 21:43:05
 * @Description
 */
package go_quick_starter

import (
	"quick-start/initializers"
	"quick-start/models"
	"testing"
)

func CreateUser(user *models.User) error {
	result := initializers.DB.Create(user)
	return result.Error
}
func GetUserByID(id uint) (*models.User, error) {
	var user models.User
	result := initializers.DB.First(&user, id)
	return &user, result.Error
}
func GetAllUsers() ([]models.User, error) {
	var users []models.User
	result := initializers.DB.Find(&users)
	return users, result.Error
}
func UpdateUser(user *models.User) error {
	result := initializers.DB.Save(user)
	return result.Error
}
func DeleteUser(id uint) error {
	result := initializers.DB.Delete(&models.User{}, id)
	return result.Error
}

// 保存新增的用户的ID
var ID int = 1

func TestCreateUser(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	user := &models.User{
		Email:    "[email protected]",
		Password: "password123",
		Username: "createuser",
	}

	err := CreateUser(user)
	if err != nil || user.ID == 0 {
		t.Errorf("Failed to create user: %v", err)
	}
	ID = int(user.ID)
}

func TestGetUserByID(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	retrievedUser, err := GetUserByID(uint(ID))
	if err != nil || retrievedUser.ID == 0 {
		t.Errorf("Failed to retrieve user: %v", err)
	}
}

func TestGetAllUsers(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	// 插入两个用户
	user1 := &models.User{Email: "[email protected]", Password: "pass", Username: "user1"}
	user2 := &models.User{Email: "[email protected]", Password: "pass", Username: "user2"}
	initializers.DB.Create(user1)
	initializers.DB.Create(user2)

	users, err := GetAllUsers()
	if err != nil || len(users) < 2 {
		t.Errorf("Failed to get all users: %v", err)
	}
}

func TestUpdateUser(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	// 更新用户
	user := &models.User{
		ID:       uint(ID),
		Email:    "[email protected]",
		Password: "password123",
		Username: "oldname",
	}

	err := UpdateUser(user)
	if err != nil {
		t.Errorf("Failed to update user: %v", err)
	}
}

func TestDeleteUser(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	// 删除用户
	err := DeleteUser(uint(ID))
	if err != nil {
		t.Errorf("Failed to delete user: %v", err)
	}
}

密码加密

为了保证用户信息安全,需要对用户的密码进行加密处理。

import (
	"fmt"
	"golang.org/x/crypto/bcrypt"
	"os"
	"quick-start/initializers"
	"quick-start/models"
	"testing"
	"time"
)

func TestBcryptPasswordEncode(t *testing.T) {
	user := &models.User{
		Email:    "[email protected]",
		Password: "password123",
		Username: "createuser",
	}

	// 密码加盐(加密)
	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
	if err != nil {
		t.Errorf("Failed to hash password")
	}
	fmt.Println(hash)
	fmt.Println(string(hash))
}

创建JWT令牌

JWT(JSON Web Token)是一种开放标准,用于在网络应用环境间安全地传递声明。创建JWT令牌是为了实现用户身份验证。

import (
	"fmt"
	"github.com/golang-jwt/jwt/v4"
	"golang.org/x/crypto/bcrypt"
	"os"
	"quick-start/initializers"
	"quick-start/models"
	"testing"
	"time"
)

func TestJWT(t *testing.T) {
	user := &models.User{
		Email:    "[email protected]",
		Password: "password123",
		Username: "createuser",
	}

	// 密码加盐(加密)
	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
	if err != nil {
		t.Errorf("Failed to hash password")
	}
	fmt.Println(hash)
	fmt.Println(string(hash))

	// 校验密码
	err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(user.Password))
	if err != nil {
		t.Errorf("Invalid password")
	}

	// 生成jwt令牌(传输协议用https,加密传输,防止jwt泄露)
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"sub": user.ID,
		// 过期时间
		"exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
	})

	// 传入密钥,加密
	tokenStr, err := token.SignedString([]byte(os.Getenv("SECRET")))
	if err != nil {
		t.Errorf("Failed to generate token")
	}
	fmt.Println(tokenStr)
}

进行测试

帖子信息表

增删改查

实现对帖子信息的基本操作:增加、删除、修改和查询。

// /post_test.go
package go_quick_starter

import (
	"quick-start/initializers"
	"quick-start/models"
	"testing"
)

func CreatePost(post *models.Post) error {
	result := initializers.DB.Create(post)
	return result.Error
}
func GetPostByID(id uint) (*models.Post, error) {
	var post models.Post
	result := initializers.DB.First(&post, id)
	return &post, result.Error
}
func GetAllPosts() ([]models.Post, error) {
	var posts []models.Post
	result := initializers.DB.Find(&posts)
	return posts, result.Error
}
func UpdatePost(post *models.Post) error {
	result := initializers.DB.Save(post)
	return result.Error
}
func DeletePost(id uint) error {
	result := initializers.DB.Delete(&models.Post{}, id)
	return result.Error
}

func TestCreatePost(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	post := &models.Post{
		Title: "test",
		Body:  "test body",
	}

	err := CreatePost(post)
	if err != nil || post.ID == 0 {
		t.Errorf("Failed to create user: %v", err)
	}
	ID = int(post.ID)
}

func TestGetPostByID(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	retrievedPost, err := GetPostByID(uint(ID))
	if err != nil || retrievedPost.ID == 0 {
		t.Errorf("Failed to retrieve user: %v", err)
	}
}

func TestGetAllPosts(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	// 插入两个用户
	post1 := &models.Post{
		Title: "test",
		Body:  "test body",
	}
	post2 := &models.Post{
		Title: "test",
		Body:  "test body",
	}
	initializers.DB.Create(post1)
	initializers.DB.Create(post2)

	users, err := GetAllPosts()
	if err != nil || len(users) < 2 {
		t.Errorf("Failed to get all users: %v", err)
	}
}

func TestUpdatePost(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	// 更新用户
	post := &models.Post{
		Title: "test",
		Body:  "test body",
	}

	err := UpdatePost(post)
	if err != nil {
		t.Errorf("Failed to update user: %v", err)
	}
}

func TestDeletePost(t *testing.T) {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()

	// 删除用户
	err := DeletePost(uint(ID))
	if err != nil {
		t.Errorf("Failed to delete user: %v", err)
	}
}

网络接口

定义路由

restful风格API:

HTTP 方法|操作类型|示例
GET|查询资源|获取用户列表 /api/users
POST|创建资源|创建新用户 /api/users
PUT|更新资源|更新指定用户 /api/users/{id}
DELETE|删除资源|删除指定用户 /api/users/{id}

// /main.go
/*
 * @Author Malred
 * @Date 2025-06-16 08:00:23
 * @Description
 */
package main

import (
	"github.com/gin-gonic/gin"
	"quick-start/controllers"
	"quick-start/initializers"
	"quick-start/middleware"
)

// 启动时自动调用
func init() {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()
}

func main() {
	r := gin.Default()

	r.Use(middleware.Cors())
	{
		r.GET("posts", controllers.PostsIndex)
		r.GET("posts/:id", controllers.PostsShow)
		r.GET("posts/page", controllers.PostsPage)
		r.POST("posts", middleware.RequireAuthHeader, controllers.PostsCreate)
		r.PATCH("posts/:id", middleware.RequireAuthHeader, controllers.PostsUpdate)
		r.DELETE("posts/:id", middleware.RequireAuthHeader, controllers.PostsDelete)
	}
	{
		r.GET("auth/profile", middleware.RequireAuthHeader, controllers.Validate)
		r.POST("users", controllers.Register)
		r.POST("auth/login", controllers.Login)
	}
	r.Run()
}

登陆注册

实现用户的登录和注册功能,包括身份验证和账户创建。

// controllers/user.go
/*
 * @Author Malred
 * @Date 2025-06-16 08:00:44
 * @Description
 */
package controllers

import (
	"net/http"
	"os"
	"quick-start/initializers"
	"quick-start/models"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
	"golang.org/x/crypto/bcrypt"
)

// 注册
func Register(c *gin.Context) {
	var body struct {
		Username        string `json:"username" validate:"min=5"`
		Password        string `json:"password" validate:"min=8"`
		RetypedPassword string `json:"retypedPassword" validate:"min=8,eqfield=Password"`
		Email           string `json:"email" validate:"email"`
	}

	// 将json数据绑定到结构体
	c.Bind(&body)

	// 校验
	err := validate.Struct(body)
	if err != nil {
		c.JSON(400, gin.H{
			"error": err.Error(),
		})
		return
	}

	var user models.User
	initializers.DB.
		Where("email = ?", body.Email).
		First(&user)

	// 已存在(其实username也是唯一的,设计在表里了)
	if user != (models.User{}) {
		c.JSON(500, gin.H{
			"error": "the email is already exist!",
		})
		return
	}

	// hash the password
	hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), 10)
	if err != nil {
		c.JSON(500, gin.H{
			"error": "failed to hash password",
		})
		return
	}

	user = models.User{
		Username: body.Username,
		Password: string(hash),
		Email:    body.Email,
	}

	result := initializers.DB.
		Create(&user)

	if result.Error != nil {
		c.JSON(500, gin.H{
			"error": "failed to create user",
		})
		return
	}

	c.JSON(200, gin.H{
		"user": user,
	})
}

// 登录
func Login(c *gin.Context) {
	var body struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}

	if err := c.Bind(&body); err != nil {
		c.JSON(400, gin.H{
			"error": "failed to bind the body!",
		})
		return
	}

	var user models.User
	initializers.DB.
		Where("username = ?", body.Username).
		Find(&user)

	if user == (models.User{}) {
		c.JSON(500, gin.H{
			"error": "con't find the user!",
		})
		return
	}

	// 校验密码
	err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password))
	if err != nil {
		c.JSON(400, gin.H{
			"error": "failed to hash password",
		})
		return
	}
	// 生成jwt令牌(传输协议用https,加密传输,防止jwt泄露)
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"sub": user.ID,
		// 过期时间
		"exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
	})
	// 传入密钥,加密
	tokenStr, err := token.SignedString([]byte(os.Getenv("SECRET")))
	if err != nil {
		c.JSON(400, gin.H{
			"error": "Failed to create token",
		})
		return
	}
	// 设置cookie
	c.SetSameSite(http.SameSiteLaxMode)
	c.SetCookie("Authorization", tokenStr, 3600*24*30, "", "", false, true)
	c.JSON(200, gin.H{
		"token":  tokenStr,
		"userId": user.ID,
	})
}

// 校验,返回用户信息
func Validate(c *gin.Context) {
	id, _ := c.Get("id")
	uname, _ := c.Get("username")
	email, _ := c.Get("email")
	c.JSON(200, gin.H{
		"id":       id,
		"username": uname,
		"email":    email,
	})
}

帖子信息接口

提供获取、发布、更新和删除帖子信息的 API 接口。

// controllers/post.go
/*
 * @Author Malred
 * @Date 2025-06-16 08:00:47
 * @Description
 */
package controllers

import (
	"quick-start/initializers"
	"quick-start/models"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/go-playground/validator/v10"
)

var validate *validator.Validate

func init() {
	validate = validator.New()
}

// 添加
func PostsCreate(c *gin.Context) {
	// 获取json数据
	var body struct {
		Body  string `json:"body" validate:"min=6,max=255"`
		Title string `json:"title" validate:"min=3,max=20"`
	}
	// 将json数据绑定到结构体
	c.Bind(&body)

	// 校验
	err := validate.Struct(body)
	if err != nil {
		c.JSON(400, gin.H{
			"error": err.Error(),
		})
		return
	}

	post := models.Post{
		Title: body.Title,
		Body:  body.Body,
	}

	// 创建
	result := initializers.DB.Create(&post)
	if result.Error != nil {
		c.Status(400)
		return
	}

	c.JSON(200, gin.H{
		"post": post,
	})
}

// 查询所有
func PostsIndex(c *gin.Context) {
	var posts []models.Post
	initializers.DB.Find(&posts)

	c.JSON(200, gin.H{
		"posts": posts,
	})
}

// 分页查询
func PostsPage(c *gin.Context) {
	limitStr := c.Query("limit")
	curPageStr := c.Query("currentPage")

	limit, err := strconv.Atoi(limitStr)
	if err != nil {
		c.Status(500)
		return
	}
	curPage, err := strconv.Atoi(curPageStr)
	if err != nil {
		c.Status(500)
		return
	}

	var posts []models.Post
	initializers.DB.
		Scopes(models.Paginate(curPage, limit)).
		Find(&posts)

	c.JSON(200, gin.H{
		"posts": posts,
	})
}

// 根据id查询
func PostsShow(c *gin.Context) {
	id := c.Param("id")

	var post models.Post
	initializers.DB.First(&post, id)

	c.JSON(200, gin.H{
		"post": post,
	})
}

// 修改
func PostsUpdate(c *gin.Context) {
	id := c.Param("id")

	var body struct {
		Body  string `json:"body" validate:"min=6,max=255"`
		Title string `json:"title" validate:"min=3,max=20"`
	}
	// 将json数据绑定到结构体
	c.Bind(&body)

	// 校验
	err := validate.Struct(body)
	if err != nil {
		c.JSON(400, gin.H{
			"error": err.Error(),
		})
		return
	}

	var post models.Post
	initializers.DB.First(&post, id)

	initializers.DB.Model(&post).Updates(models.Post{
		Title: body.Title,
		Body:  body.Body,
	})

	c.JSON(200, gin.H{
		"post": post,
	})
}

// 删除
func PostsDelete(c *gin.Context) {
	id := c.Param("id")

	initializers.DB.Delete(&models.Post{}, id)

	c.Status(200)
}

鉴权和跨域中间件

鉴权中间件用于验证用户身份,确保只有授权用户才能访问特定资源。
跨域中间件用于解决跨域请求问题,允许来自不同域的请求访问服务器资源。

// middleware/auth.go
/*
 * @Author Malred
 * @Date 2025-06-16 08:01:36
 * @Description
 */
package middleware

import (
	"fmt"
	"net/http"
	"os"
	"quick-start/initializers"
	"quick-start/models"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
)

// 解决跨域问题
func RequireAuthHeader(c *gin.Context) {
	// 获取token
	tokenStr := c.Request.Header.Get("Authorization")
	fmt.Println(tokenStr[:7])
	if tokenStr[:6] != "Bearer" {
		c.AbortWithStatus(http.StatusUnauthorized)
	}
	// 验证token
	token, err := jwt.Parse(tokenStr[7:], func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(os.Getenv("SECRET")), nil
	})
	if err != nil {
		fmt.Println(token)
		c.AbortWithStatus(http.StatusUnauthorized)
	}
	if cliams, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		// 过期
		if float64(time.Now().Unix()) > cliams["exp"].(float64) {
			c.AbortWithStatus(http.StatusUnauthorized)
		}
		// 根据jwt携带消息(userId)查询数据库
		var user models.User
		initializers.DB.First(&user, cliams["sub"])
		if user.ID == 0 {
			c.AbortWithStatus(http.StatusUnauthorized)
		}
		// c.Set("user", user)
		c.Set("id", user.ID)
		c.Set("username", user.Username)
		c.Set("email", user.Email)
		c.Next()
	} else {
		c.AbortWithStatus(http.StatusUnauthorized)
	}
}
// middleware/cors.go
/*
 * @Author Malred
 * @Date 2025-06-16 08:01:40
 * @Description
 */
package middleware

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

// 解决跨域问题
func Cors() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := c.Request.Method
		origin := c.Request.Header.Get("Origin")
		c.Header("Access-Control-Allow-Origin", origin)
		// c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Allow-Headers", "*")
		c.Header("Access-Control-Allow-Methods", "*")
		c.Header("Access-Control-Expose-Headers", "Content-Length,Access-Control-Allow-Origin,Access-Control-Allow-Headers,Content-Type")
		c.Header("Access-Control-Max-Age", "3600")
		c.Header("Access-Control-Allow-Credentials", "true")
		//放行索引options
		if method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
		}
		//处理请求
		c.Next()
	}
}

使用APIfox进行测试

APIfox是一个接口测试工具
apifox
本案例的接口我已经共享了:
接口文档

目前较成熟的二开框架

暂时不了解, 可以看看别人的回答
link

社群

你可以在这些平台联系我:

  • bili: 刚子哥forever
  • 企鹅群: 940263820
  • gitee: gitee
  • 博客: malcode-site
  • 邮箱: [email protected]
  • 知乎: 乐妙善哉居士
  • csdn: 飞鸟malred

你可能感兴趣的:(golang,前端,开发语言)