Go語言從入門到精通 -【web項目實戰篇】- 完整的web項目


本節核心內容

  • 介紹項目的目錄結構
  • 介紹包括文件讀取、日志、mysql、路由、http、json數據處理等技術
  • 介紹自定義錯誤碼
  • 通過實戰代碼演練創建一個基礎的web項目

本小節視頻教程和代碼:百度網盤,密碼z2ua

可先下載視頻和源碼到本地,邊看視頻邊結合源碼理解后續內容,邊學邊練。

HTTP API 服務器啟動流程

image

目錄結構

在開發中,一個良好的目錄結構是很重要的,好的目錄結構不僅能使項目結構清晰,也能讓后加入者快速了解項目,便於上手。

├── conf                         # 配置文件統一存放目錄
│   ├── config.yaml              # 配置文件
├── config                       # 專門用來處理配置和配置文件的Go package
│   └── config.go                 
├── db.sql                       # 在部署新環境時,可以登錄MySQL客戶端,執行source db.sql創建數據庫和表
├── handler                      # 類似MVC架構中的C,用來讀取輸入,並將處理流程轉發給實際的處理函數,最后返回結果
│   ├── handler.go
│   ├── sd                       # 健康檢查handler
│   │   └── check.go 
│   └── user                     # 核心:用戶業務邏輯handler
│       ├── create.go            # 新增用戶
│       └── user.go              # 存放用戶handler公用的函數、結構體等
├── main.go                      # Go程序唯一入口
├── Makefile                     # Makefile文件,一般大型軟件系統都是采用make來作為編譯工具
├── model                        # 數據庫相關的操作統一放在這里,包括數據庫初始化和對表的增刪改查
│   ├── init.go                  # 初始化和連接數據庫
│   ├── model.go                 # 存放一些公用的go struct
│   └── user.go                  # 用戶相關的數據庫CURD操作
├── pkg                          # 引用的包
│   ├── constvar                 # 常量統一存放位置
│   │   └── constvar.go
│   ├── errno                    # 錯誤碼存放位置
│   │   ├── code.go
│   │   └── errno.go
├── README.md                    # API目錄README
├── router                       # 路由相關處理
│   ├── middleware               # API服務器用的是Gin Web框架,Gin中間件存放位置
│   │   ├── header.go
│   │   ├── logging.go
│   │   └── requestid.go
│   └── router.go                # 路由
├── service                      # 實際業務處理函數存放位置
│   └── service.go
├── util                         # 工具類函數存放目錄
│   ├── util.go 
│   └── util_test.go
└── vendor                         # vendor目錄用來管理依賴包
    ├── github.com
    ├── golang.org
    ├── gopkg.in
    └── vendor.json

讀取配置文件和配置日志對象

配置文件:

mysql:
  #最大空閑連接數
  max_idle_conns: 50
  #鏈接
  source_name: root:root@tcp(127.0.0.1:3306)/devops?parseTime=true&charset=utf8&loc=Local
addr: 127.0.0.1:8083                  # HTTP綁定端口

代碼:

package config

import (
	"github.com/spf13/viper"
	"time"
	"os"
	"log"
)

// LogInfo 初始化日志配置
func LogInfo()   {
	file := "./" + time.Now().Format("2006-01-02") + ".log"
	logFile, _ := os.OpenFile(file,os.O_RDWR| os.O_CREATE| os.O_APPEND, 0766)
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
	log.SetOutput(logFile)
}

// Init 讀取初始化配置文件
func Init() error {

	// 初始化配置文件
	if err := Config(); err != nil {
		return err
	}

	// 初始化日志包
	LogInfo()
	return nil
}

// Config viper解析配置文件
func Config() error  {
	viper.AddConfigPath("conf")
	viper.SetConfigName("config")
	if err := viper.ReadInConfig(); err != nil {
		return err
	}

	return nil
}

配置Mysql

准備表數據

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT '',
  `age` int(11) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4

init初始化代碼:

package model

import (
	"database/sql"
	"log"

	_ "github.com/go-sql-driver/mysql" //這個引用是必不可少的,因為需要調用driver.go文件里的init方法來提供一個數據庫驅動程序
	"github.com/spf13/viper"
)

var DB *sql.DB     //全局變量,這樣可以在別處調用

func Init() error {

	var err error

	//這行代碼的作用就是初始化一個sql.DB對象
	DB ,err = sql.Open("mysql", viper.GetString("mysql.source_name"))
	if nil != err {
		return err
	}

	//設置最大超時時間
	DB.SetMaxIdleConns(viper.GetInt("mysql.max_idle_conns"))

	//建立鏈接
	err = DB.Ping()
	if nil != err{
		return err
	}else{
		log.Println("Mysql Startup Normal!")
	}
	return nil
}

model代碼:

package model

import (
	"log"
	"fmt"
)

//	Insert 插入操作
func Insert(sql string,args... interface{})(int64,error) {
	stmt, err := DB.Prepare(sql)
	defer stmt.Close()
	if err != nil{
		return 1,err
	}
	result, err := stmt.Exec(args...)
	if err != nil{
		return 1,err
	}
	id, err := result.LastInsertId()
	if err != nil{
		return 1,err
	}
	fmt.Printf("插入成功,ID為%v\n",id)
	return id,nil
}

//	Delete 刪除操作
func Delete(sql string,args... interface{})  {
	stmt, err := DB.Prepare(sql)
	defer stmt.Close()
	CheckErr(err, "SQL語句設置失敗")
	result, err := stmt.Exec(args...)
	CheckErr(err, "參數添加失敗")
	num, err := result.RowsAffected()
	CheckErr(err,"刪除失敗")
	fmt.Printf("刪除成功,刪除行數為%d\n",num)
}

//	Update 修改操作
func Update(sql string,args... interface{})  {
	stmt, err := DB.Prepare(sql)
	defer stmt.Close()
	CheckErr(err, "SQL語句設置失敗")
	result, err := stmt.Exec(args...)
	CheckErr(err, "參數添加失敗")
	num, err := result.RowsAffected()
	CheckErr(err,"修改失敗")
	fmt.Printf("修改成功,修改行數為%d\n",num)
}

// CheckErr 用來校驗error對象是否為空
func CheckErr(err error,msg string)  {
	if nil != err {
		log.Panicln(msg,err)
	}
}

准備數據庫表和對應實體類對象

user.db數據:

/*
Navicat MySQL Data Transfer

Source Server         : localhost
Source Server Version : 50625
Source Host           : 127.0.0.1:3306
Source Database       : test

Target Server Type    : MYSQL
Target Server Version : 50625
File Encoding         : 65001

Date: 2019-02-17 22:47:26
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(20) NOT NULL DEFAULT '',
  `password` varchar(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `id` (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', '12164');

實體類:

type User struct {
	UserName 	string `json:"user_name"`
	Password 	string `json:"password"`
}

自定義錯誤碼

error相關代碼:

package errno

import "fmt"

type Errno struct {
	Code    int
	Message string
}

func (err Errno) Error() string {
	return err.Message
}

// Err represents an error
type Err struct {
	Code    int
	Message string
	Err     error
}

func New(errno *Errno, err error) *Err {
	return &Err{Code: errno.Code, Message: errno.Message, Err: err}
}

func (err *Err) Add(message string) error {
	err.Message += " " + message
	return err
}

func (err *Err) Addf(format string, args ...interface{}) error {
	err.Message += " " + fmt.Sprintf(format, args...)
	return err
}

func (err *Err) Error() string {
	return fmt.Sprintf("Err - code: %d, message: %s, error: %s", err.Code, err.Message, err.Err)
}

func IsErrUserNotFound(err error) bool {
	code, _ := DecodeErr(err)
	return code == ErrUserNotFound.Code
}

func DecodeErr(err error) (int, string) {
	if err == nil {
		return OK.Code, OK.Message
	}

	switch typed := err.(type) {
	case *Err:
		return typed.Code, typed.Message
	case *Errno:
		return typed.Code, typed.Message
	default:
	}

	return InternalServerError.Code, err.Error()
}

定義code錯誤碼:

package errno

var (
	// Common errors
	OK                  = &Errno{Code: 0, Message: "OK"}
	InternalServerError = &Errno{Code: 10001, Message: "Internal server error"}
	ErrBind             = &Errno{Code: 10002, Message: "Error occurred while binding the request body to the struct."}

	ErrValidation = &Errno{Code: 20001, Message: "Validation failed."}
	ErrDatabase   = &Errno{Code: 20002, Message: "Database error."}

	// user errors
	ErrUserNotFound      = &Errno{Code: 20101, Message: "The user was not found."}
	ErrPasswordIncorrect = &Errno{Code: 20102, Message: "The password was incorrect."}
)

定義響應的handler

package handler

import (
	"net/http"

	"apiserver/pkg/errno"

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

type Response struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

func SendResponse(c *gin.Context, err error, data interface{}) {
	code, message := errno.DecodeErr(err)

	// always return http.StatusOK
	c.JSON(http.StatusOK, Response{
		Code:    code,
		Message: message,
		Data:    data,
	})
}

創建路由

使用中間件控制請求頭:

middleware代碼:

package middleware

import (
	"net/http"
	"time"

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

// NoCache is a middleware function that appends headers
// to prevent the client from caching the HTTP response.
func NoCache(c *gin.Context) {
	c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
	c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
	c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
	c.Next()
}

// Options is a middleware function that appends headers
// for options requests and aborts then exits the middleware
// chain and ends the request.
func Options(c *gin.Context) {
	if c.Request.Method != "OPTIONS" {
		c.Next()
	} else {
		c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
		c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
		c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
		c.Header("Content-Type", "application/json")
		c.AbortWithStatus(200)
	}
}

// Secure is a middleware function that appends security
// and resource access headers.
func Secure(c *gin.Context) {
	c.Header("Access-Control-Allow-Origin", "*")
	c.Header("X-Frame-Options", "DENY")
	c.Header("X-Content-Type-Options", "nosniff")
	c.Header("X-XSS-Protection", "1; mode=block")
	if c.Request.TLS != nil {
		c.Header("Strict-Transport-Security", "max-age=31536000")
	}

	// Also consider adding Content-Security-Policy headers
	// c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
}

router代碼:

package router

import (
	"net/http"

	"myapi/service"
	"myapi/router/middleware"

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

//InitRouter 初始化路由
func InitRouter(g *gin.Engine) {
	middlewares := []gin.HandlerFunc{}
	// Middlewares.
	g.Use(gin.Recovery())
	g.Use(middleware.NoCache)
	g.Use(middleware.Options)
	g.Use(middleware.Secure)
	g.Use(middlewares...)
	// 404 Handler.
	g.NoRoute(func(c *gin.Context) {
		c.String(http.StatusNotFound, "The incorrect API route.")
	})


	// The health check handlers
	router := g.Group("/user")
	{
		router.POST("/addUser", service.AddUser)					//添加用戶
		router.POST("/selectUser", service.SelectUser)			//查詢用戶
	}

}


處理路由

service層

package service

import (
	"github.com/gin-gonic/gin"
	"myapi/model"
	. "myapi/handler"
	"myapi/pkg/error"
	"fmt"
)

func AddUser(c *gin.Context)  {
	var r model.User
	if err := c.Bind(&r); err != nil {
		SendResponse(c, errno.ErrBind, nil)
		return
	}
	u := model.User{
		UserName: r.UserName,
		Password: r.Password,
	}
	// Validate the data.
	if err := u.Validate(); err != nil {
		SendResponse(c, errno.ErrValidation, nil)
		return
	}
	// Insert the user to the database.
	if _,err := u.Create(); err != nil {
		SendResponse(c, errno.ErrDatabase, nil)
		return
	}
	// Show the user information.
	SendResponse(c, nil, u)
}

// SelectUser 查詢用戶
func SelectUser(c *gin.Context)  {
	name := c.Query("user_name")
	if name == ""{
		SendResponse(c, errno.ErrValidation, nil)
		return
	}
	var  user model.User
	if err := user.SelectUserByName(name);nil != err {
		fmt.Println(err)
		SendResponse(c, errno.ErrUserNotFound, nil)
		return
	}
	// Validate the data.
	if err := user.Validate(); err != nil {
		SendResponse(c, errno.ErrUserNotFound, nil)
		return
	}

	SendResponse(c, nil, user)
}


數據庫層

package model

import (
	"errors"
	"myapi/pkg/error"
	"encoding/json"
	"log"
)

type User struct {
	UserName 	string `json:"user_name"`
	Password 	string `json:"password"`
}

func (user *User)SelectUserByName(name string)error {
	stmt,err := DB.Prepare("SELECT user_name,password FROM user WHERE user_name=?")
	if err != nil {
		return err
	}
	defer stmt.Close()
	rows, err := stmt.Query(name)
	defer rows.Close()
	if err != nil {
		return err
	}
	// 數據處理
	for rows.Next() {
		rows.Scan( &user.UserName, &user.Password)
	}
	if err := rows.Err(); err != nil {
		return err
	}
	return nil
}

// Validate the fields.
func (u *User) Validate() error {
	if u.UserName =="" || u.Password ==""{
		return errors.New(errno.ErrValidation.Message)
	}
	return nil
}
func (user *User) Create() (int64,error)  {
	id,err := Insert("INSERT INTO  user(user_name,password) values (?,?)", user.UserName, user.Password)
	if err != nil {
		return 1,err
	}

	return id,nil
}

func (user *User)UserToJson()string  {
	jsonStr, err := json.Marshal(user)
	if err != nil {
		log.Println(err)
	}
	return string(jsonStr)
}

func (user *User)JsonToUser(jsonBlob string)error  {
	err := json.Unmarshal([]byte(jsonBlob), &user)
	if err != nil {
		return err
	}
	return nil
}

main方法

package main

import (
	"myapi/config"
	"myapi/model"
	"github.com/gin-gonic/gin"
	"log"
	"github.com/spf13/viper"
	"myapi/router"
)

func main() {
	if err := config.Init();err != nil {
		panic(err)
	}

	if err := model.Init();err != nil {
		panic(err)
	}
	//g := gin.Default()

	// Set gin mode.
	gin.SetMode(viper.GetString("runmode"))

	// Create the Gin engine.
	g := gin.New()

	router.InitRouter(g)
	log.Printf("Start to listening the incoming requests on http address: %s\n", viper.GetString("addr"))
	//log.Println(http.ListenAndServe(viper.GetString("addr"), g).Error())
	if err := g.Run(viper.GetString("addr"));err != nil {log.Fatal("ListenAndServe:", err)}

}

Json格式處理

func (user *User)UserToJson()string  {
	jsonStr, err := json.Marshal(user)
	if err != nil {
		log.Println(err)
	}
	return string(jsonStr)
}

func (user *User)JsonToUser(jsonBlob string)error  {
	err := json.Unmarshal([]byte(jsonBlob), &user)
	if err != nil {
		return err
	}
	return nil
}

小節

本小節主要通過結合之前講過的知識點,將知識點串聯起來,帶領大家實現一個簡單的項目,創建一個項目的步驟可大致分為下面幾步:

  • 創建數據庫表
  • 創建項目和目錄結構
  • 讀取配置文件和配置日志對象
  • 配置Mysql
  • 自定義錯誤碼
  • 定義響應的handler層
  • 定義路由
  • 處理路由
    • service層
    • sql層
  • 一些數據格式的處理
    • 如結構體和Json的格式處理
  • main方法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM