本節核心內容
- 介紹項目的目錄結構
- 介紹包括文件讀取、日志、mysql、路由、http、json數據處理等技術
- 介紹自定義錯誤碼
- 通過實戰代碼演練創建一個基礎的web項目
本小節視頻教程和代碼:百度網盤,密碼z2ua
可先下載視頻和源碼到本地,邊看視頻邊結合源碼理解后續內容,邊學邊練。
HTTP API 服務器啟動流程
目錄結構
在開發中,一個良好的目錄結構是很重要的,好的目錄結構不僅能使項目結構清晰,也能讓后加入者快速了解項目,便於上手。
├── 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方法