Go语言从入门到精通 -【web项目实战篇】- 完整的web项目

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方法
posted @ 2019-03-25 19:58 回首笑人间 阅读( ...) 评论( ...)   编辑 收藏
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

回首笑人间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值