Go语言打造高并发web即时聊天(IM-Instant Messaging)应用

Go语言打造高并发web即时聊天(IM)应用-支持10万人同时在线

1.技术栈

1.1.前端技术

  • h5
    • ajax
      发送文字- 上传文字
      发送图片-上传图片
    • 使用h5获取音频
    • 使用h5发送WebSocket消息
  • Vue制作单页app
  • mui、css

1.2. 后端技术

  • WebSocket组件
    • http协议握手升级成WebSocket协议
  • channel、gorountine
    • 提高系统的并发性
  • template
    • 模板渲染技术

1.3.架构技术

  • Nginx反向代理
  • 消息总线MQ/Redis
  • UDP协议/HTTP2协议

2. 需求分析

2.1.基本需求

  • 2.1.1.发送、接收
    • 1.功能界面显示
      发送和接收界面的不同,发送的语音、图片、文字样式等等界面

    • 2.发送需求
      实现资源标准化编码

      • 资源信息采集并标准化,转换成 content/url
      • 资源编码,拼接成一个消息体(json/xml)


        资源标准化
    • 3.确保消息体的可扩展性

      • 兼容基础媒介:图片、文字、语音(url/pic/content/num)
      • 承载大量新业务,业务扩展不能对现有业务产生影响
      • 红包/打卡/签到等本质上是消息,但是内容却是不一样的
// 消息体核心代码快-go语言结构体
type Message struct {
    // 消息ID
    Id int64 `json:"id,omitempty" form:"id"`
    // 消息发送方
    UserId int64 `json:"userid,omitempty" form:"userid"`
    // 群聊还是私聊标记
    Cmd int `json:"cmd,omitempty" form:"cmd"`
    // 对端ID,或者群ID
    Dstid int64 `json:"dstid,omitempty" form:"dstid"`
    // 消息样式
    Media int `json:"media,omitempty" form:"media"`
    // 消息的内容
    Content string `json:"content,omitempty" form:"content"`
    // 预览图片
    Pic string `json:"pic,omitempty" form:"pic"`
    // 服务的URL
    Url string `json:"url,omitempty" form:"url"`
    // 简单的描述
    Memo string `json:"memo,omitempty" form:"memo"`
    // 数字、数值相关
    Amount int `json:"amount,omitempty" form:"amount"`
}
  • 2.1.1.4.接收消息并解析显示
    • 接收到消息体【json】并进行解析
    • 区分不同形式(图片/文字/语音)
    • 界面显示发出和接收
  • 2.1.2.群聊
    • 基础功能无区别
    • 一条群聊的消息要在多个参与群聊的终端及时接收到
    • 服务器流量的计算
    • 服务器负载分析
假设群聊中:
用户A发送图片数据512k
100人在线群人员同时收到
512kb * 100 = 1024kb * 50 = 50M
假设有1024个群
1024* 50 M = 50G
  • 服务器流量的计算->服务器负载分析的【解决方案】
    • 1.使用缩略图(512k-->51.2k)提高单图下载和渲染速度,提供给用户【查看原图的】功能
    • 2.使用资源分离的方式,将资源服务和应用服务分离,提高资源服务并发能力,使用云服务(阿里云等)作为资源服务器,利用阿里云的海量并发服务实现资源的访问速度提高用户体验
    • 3.压缩消息体,发送文件路径而不是整个文件
  • 2.1.3.高并发
    • 单机状态+分布式+弹性扩容
    • 单机并发性能最优
    • 海量用户采用分布式部署
    • 应对突发事件进行弹性扩容

3.IM系统架构

IM系统的一般结构

实现及重点

网络结构

4. WebSocket

4.1. 选型

github.com/joewalnes/websocketd
github.com/gorilla/websocket

以上两个包非官方,但是都依赖于下面的官方的扩展包下的net包中WebSocket。

github.com/golang/net

4.2.安装net包

由于大陆境内有强的缘故,所以对于golang.org/x/net需要手动创建目录后,然后使用git clone方式进行下载

  • 进入$GOPATH/src/目录下:
cd $GOPATH/src/
  • 创建文件目录golang.org/x/
mkdir -p golang.org/x/
  • 进入golang.org/x/目录
cd golang.org/x/
  • 执行克隆net包
git clone https://github.com/golang/net.git
  • 查看并确认
ls
下载安装golang的扩展包下的net包

4.3. 安装gorilla/websocket

本次选用gorilla/websocket为WebSocket服务

  • 使用go的get命令进行安装
go get -u -v github.com/gorilla/websocket
安装gorilla/websocket

4.4.鉴权

判断id和token是否一致,一致则鉴权成功

4.4.1. 鉴权成功

鉴权成功

4.4.2.鉴权失败

鉴权失败

4.4.3.鉴权接入/用户信息

接入鉴权/用户信息表

4.4.4.鉴权接入/conn

鉴权接入/conn

4.4.5.conn的维护

最简单的Conn的维护,让userid和conn形成一个映射关系,一个map【ClientMap】的键是int64类型的userid,值是conn的指针,实际开发过程中,一个用户的信息远不止这些,所以定义了一个ClientNode的结构体,用来存放conn的指针,以及用户的各种其他信息,所以对map【clientMap】做了升级,key为int64类型的用户id,值为clientNode的结构体指针

conn的维护

4.5.后端消息的接收

后端消息的接收

4.6.后端消息的发送

后端消息的发送

4.7.前端使用WebSocket

前端使用WebSocket

4.8.WebSocket的心跳机制

WebSocket的心跳机制

4.9.前端消息的发送

前端消息的发送

前端发送消息格式
前端消息的队列发送

4.10. 前端消息的接收

前端消息的接收

4.11.流程

流程

5.单机支持高并发

5.1.设计高质量的代码优化map

  1. 由于对map的频繁读写,会存在一个安全性问题,所以要设计稿质量的代码来优化map
  • 使用读写锁【读共享,写独占】
  • 读写锁的使用场景:读的次数多写的次数少的场合
  1. map不要太大
  • 一个map维持在一个特定的大小即可,太大了毫无意义可言


    map优化

5.2.突破系统的瓶颈优化最大连接数

  • 系统选用linux
  • 解除linux系统的最大文件数


    优化最大连接数

5.3.优化CPU资源的使用

  • 降低json编码频次
  • 做到一次编码处处使用


    优化CPU资源使用

5.4.优化IO资源的使用

  • 合并写数据库次数
  • 优化对数据库的读操作
  • 尽可能多的使用缓存技术
    优化IO资源的使用

5.5.应用服务和资源服务分离

  • 应用服务系统提供动态资源服务
  • 资源服务【图片、文件、css、js等】部署在第三方云服务平台上


    应用服务和资源服务分离

6.golang web

6.1. web http编程核心API

  • 请求格式和回调函数绑定
// 绑定请求和处理函数
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
   DefaultServeMux.HandleFunc(pattern, handler)
}
// pattern string:请求的路径
// handler func(ResponseWriter, *Request):回调函数、处理函数

// 启动web服务器:监听并提供服务
func ListenAndServe(addr string, handler Handler) error {
       server := &Server{Addr: addr, Handler: handler}
       return server.ListenAndServe()
   }
// addr string:监听的IP+port
// handler Handler:回调函数,路由,如果没有自定义路由,可以传入nil来调用默认的路由
  • 测试demo
package main

import (
   "io"
   "log"
   "net/http"
)

func main() {

   // 绑定请求和处理函数
   http.HandleFunc("/user/login", func(writer http.ResponseWriter, request *http.Request) {
       // 执行数据库操作
       // 逻辑处理
       // restful风格API,返回JSON/XML
       io.WriteString(writer, "hello world!")
   })
   // 启动web服务器:监听并提供服务
   if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
       log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
   }
}

启动服务器,并在终端进行测试

  • demo的终端测试:
curl http://127.0.0.1:8080/user/login/
终端测试

7.实现后端登录接口

7.1.后端登录接口API

业务说明
业务名称 登录
请求格式 /user/login
请求参数 mobile:用户手机号;password:用户密码
返回json {"code":0,"msg":"提示信息","data":{"id":111,"token":333333 }}
  • 返回的json格式数据
{
    "code":0,// 0:成功,-1:失败
    "msg":"提示信息",// 用户名或密码错误等等
    "data":{
        "id":111,// 用户id
        "token":333333 // 鉴权因子,在WebSocket接入的时候用到
    }
}

7.1.1.登录接口API知识点梳理

7.1.1.1. 获取前端传递的参数

// 获得参数
request.ParseForm()
// 解析参数
mobile := request.PostForm.Get("mobile")
password := request.PostForm.Get("password")

7.1.1.2. 返回json格式数据给前端

// 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
writer.Header().Set("Content-Type", "application/json")
// 2.设置header的响应状态码 - 成功-200
writer.WriteHeader(http.StatusOK)
// 3. 返回JSON数据
writer.Write([]byte(str))

7.1.1.3.代码

package main

import (
    "log"
    "net/http"
)

func main() {

    // 绑定请求和处理函数
    http.HandleFunc("/user/login", func(writer http.ResponseWriter, request *http.Request) {
        // 执行数据库操作
        // 逻辑处理
        // restful风格API,返回JSON/XML
        // 获得参数
        request.ParseForm()
        // 解析参数
        mobile := request.PostForm.Get("mobile")
        password := request.PostForm.Get("password")

        // 定义简单校验的标记
        loginOk := false
        if mobile == "17500000000" && password == "123456" {
            loginOk = true
        }
        // 默认的成功的JSON字符串
        str := `{"code":0,"data":{"id":1,"token":"test"}}`
        if !loginOk {
            // 失败的JSON字符串
            str = `{"code":-1,"msg":"用户名或密码错误"}`
        }
        // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
        writer.Header().Set("Content-Type", "application/json")
        // 2.设置header的响应状态码 - 成功-200
        writer.WriteHeader(http.StatusOK)
        // 3. 返回JSON数据
        writer.Write([]byte(str))

    })

    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }

}

  • 终端测试demo
  • 成功
curl http://127.0.0.1:8080/user/login -X POST -d "mobile=17500000000&password=123456"
成功
  • 失败
curl http://127.0.0.1:8080/user/login -X POST -d "mobile=17500000000&password=12345"
失败
  • 代码优化 -1
package main

import (
    "log"
    "net/http"
)

func userLogin(writer http.ResponseWriter, request *http.Request) {
    // 执行数据库操作
    // 逻辑处理
    // restful风格API,返回JSON/XML
    // 获得参数
    request.ParseForm()
    // 解析参数
    mobile := request.PostForm.Get("mobile")
    password := request.PostForm.Get("password")

    // 定义简单校验的标记
    loginOk := false
    if mobile == "17500000000" && password == "123456" {
        loginOk = true
    }
    // 默认的成功的JSON字符串
    str := `{"code":0,"data":{"id":1,"token":"test"}}`
    if !loginOk {
        // 失败的JSON字符串
        str = `{"code":-1,"msg":"用户名或密码错误"}`
    }
    // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
    writer.Header().Set("Content-Type", "application/json")
    // 2.设置header的响应状态码 - 成功-200
    writer.WriteHeader(http.StatusOK)
    // 3. 返回JSON数据
    writer.Write([]byte(str))

}
func main() {

    // 绑定请求和处理函数
    http.HandleFunc("/user/login", userLogin)

    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }
}
  • 代码优化-2
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

func userLogin(writer http.ResponseWriter, request *http.Request) {
    // 执行数据库操作
    // 逻辑处理
    // restful风格API,返回JSON/XML
    // 获得参数
    request.ParseForm()
    // 解析参数
    mobile := request.PostForm.Get("mobile")
    password := request.PostForm.Get("password")

    // 定义简单校验的标记
    loginOk := false
    if mobile == "17500000000" && password == "123456" {
        loginOk = true
    }
    // 成功的JSON返回
    if loginOk {
        //"data":{"id":1,"token":"test"
        data := make(map[string]interface{})
        data["id"] = 1
        data["token"] = "test"
        ResponseJson(writer, 0, data, "")
    } else {
        // 失败的JSON返回
        ResponseJson(writer, -1, nil, "用户名或密码错误")
    }
}

// 定义一个结构体
type H struct {
    Code int         `json:"code"`
    Data interface{} `json:"data,omitempty"` //omitempty:如果序列化的字段值是nil,则不进行JSON序列化,不会再JSON数据中看到
    Msg  string      `json:"msg"`
}

func ResponseJson(writer http.ResponseWriter, code int, data interface{}, msg string) {
    // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
    writer.Header().Set("Content-Type", "application/json")
    // 2.设置header的响应状态码 - 成功-200
    writer.WriteHeader(http.StatusOK)
    // 3. 返回JSON数据
    // 定义一个结构体--结构体H
    h := H{
        Code: code,
        Data: data,
        Msg:  msg,
    }
    // 将结构体转换成JSON字符串
    result, err := json.Marshal(h)
    if err != nil {
        log.Fatal("json.Marshal(h) Error:", err)
    }
    // 返回数据
    writer.Write(result)
}
func main() {
    // 绑定请求和处理函数
    http.HandleFunc("/user/login", userLogin)
    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }
}

7.2.实现前端登录页面并接入

概要 技术实现
实现静态资源服务 func FileServer(root FileSystem) Handler {return &fileHandler{root}}
模板渲染技术 template
前端技术 Vue+Mui+Ajax+Promis

7.2.1.后端代码版本1

package main

import (
    "encoding/json"
    "html/template"
    "log"
    "net/http"
)

func userLogin(writer http.ResponseWriter, request *http.Request) {
    // 执行数据库操作
    // 逻辑处理
    // restful风格API,返回JSON/XML
    // 获得参数
    request.ParseForm()
    // 解析参数
    mobile := request.PostForm.Get("mobile")
    password := request.PostForm.Get("password")

    // 定义简单校验的标记
    loginOk := false
    if mobile == "17500000000" && password == "123456" {
        loginOk = true
    }
    // 成功的JSON返回
    if loginOk {
        //"data":{"id":1,"token":"test"
        data := make(map[string]interface{})
        data["id"] = 1
        data["token"] = "test"
        ResponseJson(writer, 0, data, "")
    } else {
        // 失败的JSON返回
        ResponseJson(writer, -1, nil, "用户名或密码错误")
    }
}

// 定义一个结构体
type H struct {
    Code int         `json:"code"`
    Data interface{} `json:"data,omitempty"` //omitempty:如果序列化的字段值是nil,则不进行JSON序列化,不会再JSON数据中看到
    Msg  string      `json:"msg"`
}
func ResponseJson(writer http.ResponseWriter, code int, data interface{}, msg string) {
    // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
    writer.Header().Set("Content-Type", "application/json")
    // 2.设置header的响应状态码 - 成功-200
    writer.WriteHeader(http.StatusOK)
    // 3. 返回JSON数据
    // 定义一个结构体--结构体H
    h := H{
        Code: code,
        Data: data,
        Msg:  msg,
    }
    // 将结构体转换成JSON字符串
    result, err := json.Marshal(h)
    if err != nil {
        log.Fatal("json.Marshal(h) Error:", err)
    }
    // 返回数据
    writer.Write(result)
}
func main() {
    // 提供静态资源目录支持--当前目录
    //http.Handle("/",http.FileServer(http.Dir("./")))// 有安全风险,能让main对外暴露
    // 2.指定目录的静态资源文件支持
    http.Handle("/asset/", http.FileServer(http.Dir("./")))
    // 登录/user/login.shtml的请求--后端渲染
    http.HandleFunc("/user/login.shtml", func(w http.ResponseWriter, r *http.Request) {
        // 解析--使用模板template进行解析
        tpl, err := template.ParseFiles("./view/user/login.html")
        if err != nil {
            log.Fatal(`template.ParseFiles("./view/user/login.html") Error:`, err.Error())
        }
        // func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
        // 参数1:io.Writer
        // 参数2:自定义的模板函数名称,此处在view/user/login.html中定义的{{define "/user/login.shtml"}}名称
        // 参数3:需要给前端做的数据绑定的数据
        tpl.ExecuteTemplate(w, "/user/login.shtml", nil)
    })
    // 注册/user/register.shtml的请求--后端渲染
    http.HandleFunc("/user/register.shtml", func(w http.ResponseWriter, r *http.Request) {
        // 解析--使用模板template进行解析
        tpl, err := template.ParseFiles("./view/user/register.html")
        if err != nil {
            log.Fatal(`template.ParseFiles("./view/user/register.html") Error:`, err.Error())
        }
        // func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
        // 参数1:io.Writer
        // 参数2:自定义的模板函数名称,此处在view/user/register.html中定义的{{define "/user/register.shtml"}}名称
        // 参数3:需要给前端做的数据绑定的数据
        tpl.ExecuteTemplate(w, "/user/register.shtml", nil)

    })
    // 绑定请求和处理函数
    http.HandleFunc("/user/login", userLogin)
    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }
}

7.2.2.后端代码版本1优化

package main

import (
    "encoding/json"
    "html/template"
    "log"
    "net/http"
)

func userLogin(writer http.ResponseWriter, request *http.Request) {
    // 执行数据库操作
    // 逻辑处理
    // restful风格API,返回JSON/XML
    // 获得参数
    request.ParseForm()
    // 解析参数
    mobile := request.PostForm.Get("mobile")
    password := request.PostForm.Get("password")

    // 定义简单校验的标记
    loginOk := false
    if mobile == "17500000000" && password == "123456" {
        loginOk = true
    }
    // 成功的JSON返回
    if loginOk {
        //"data":{"id":1,"token":"test"
        data := make(map[string]interface{})
        data["id"] = 1
        data["token"] = "test"
        ResponseJson(writer, 0, data, "")
    } else {
        // 失败的JSON返回
        ResponseJson(writer, -1, nil, "用户名或密码错误")
    }
}

// 定义一个结构体
type H struct {
    Code int         `json:"code"`
    Data interface{} `json:"data,omitempty"` //omitempty:如果序列化的字段值是nil,则不进行JSON序列化,不会再JSON数据中看到
    Msg  string      `json:"msg"`
}
func ResponseJson(writer http.ResponseWriter, code int, data interface{}, msg string) {
    // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
    writer.Header().Set("Content-Type", "application/json")
    // 2.设置header的响应状态码 - 成功-200
    writer.WriteHeader(http.StatusOK)
    // 3. 返回JSON数据
    // 定义一个结构体--结构体H
    h := H{
        Code: code,
        Data: data,
        Msg:  msg,
    }
    // 将结构体转换成JSON字符串
    result, err := json.Marshal(h)
    if err != nil {
        log.Fatal("json.Marshal(h) Error:", err)
    }
    // 返回数据
    writer.Write(result)
}

//万能模板解析渲染
func RenderingView() {
    // 全局解析--使用模板template进行解析
    tpl, err := template.ParseGlob("./view/**/*") //**表示的是一个目录,*表示的是文件
    // 如果出现错误不再继续
    if err != nil {
        log.Fatal(`template.ParseGlob Error:`, err.Error())
    }
    // 循环遍历所有的模板,并执行注册
    for _, v := range tpl.Templates() {
        // 获取模板名称
        tplName := v.Name()
        http.HandleFunc(tplName, func(writer http.ResponseWriter, request *http.Request) {
            // func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
            // 参数1:io.Writer
            // 参数2:自定义的模板函数名称
            // 参数3:需要给前端做的数据绑定的数据
            tpl.ExecuteTemplate(writer, tplName, nil)
        })
    }

}
func main() {
    // 指定目录的静态资源文件支持
    http.Handle("/asset/", http.FileServer(http.Dir("./")))
    // 调用万能模板解析渲染
    RenderingView()
    // 绑定请求和处理函数
    http.HandleFunc("/user/login", userLogin)
    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }
}

7.2.3.前端代码-login

{{/*定义模板的名称,shtml表示是后端渲染的文件*/}}
{{define "/user/login.shtml"}}
    <!DOCTYPE html>
    <html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no">
        <title>登录</title>
        <link rel="stylesheet" href="/asset/plugins/mui/css/mui.css"/>
        <link rel="stylesheet" href="/asset/css/login.css"/>
        <script src="/asset/plugins/mui/js/mui.js"></script>
        <script src="/asset/js/vue.min.js"></script>
        <script src="/asset/js/util.js"></script>
    </head>
    <body>

    <header class="mui-bar mui-bar-nav">
        <h1 class="mui-title">登录</h1>
    </header>
    <div class="mui-content" id="pageapp">
        <form id='login-form' class="mui-input-group">
            <div class="mui-input-row">
                <label>账号</label>
                <input v-model="user.mobile" placeholder="请输入手机号" type="text" class="mui-input-clear mui-input">
            </div>
            <div class="mui-input-row">
                <label>密码</label>
                <input v-model="user.passwd" placeholder="请输入密码" type="password" class="mui-input-clear mui-input">
            </div>
        </form>
        <div class="mui-content-padded">
            <button @click="login" type="button" class="mui-btn mui-btn-block mui-btn-primary">登录</button>
            <div class="link-area"><a id='reg' href="register.shtml">注册账号</a> <span class="spliter">|</span> <a
                        id='forgetPassword'>忘记密码</a>
            </div>
        </div>
        <div class="mui-content-padded oauth-area">
        </div>
    </div>
    </body>
    </html>
    <script>
        var app = new Vue({
            el: "#pageapp",
            data: function () {
                return {
                    user: {
                        mobile: "",
                        passwd: ""
                    }
                }
            },
            methods: {
                login: function () {
                    //检测手机号是否正确
                    console.log("login")
                    //检测密码是否为空

                    //网络请求
                    //封装了promis
                    util.post("/user/login", this.user).then(res => {
                        console.log(res)
                        if (res.code != 0) {
                            mui.toast(res.msg)
                        } else {
                            //location.replace("//127.0.0.1/demo/index.shtml")
                            mui.toast("登录成功,即将跳转")
                        }
                    })
                },
            }
        })
    </script>
{{end}}

7.2.4.前端代码-register

{{/*定义模板的名称,shtml表示是后端渲染的文件*/}}
{{define "/user/register.shtml"}}
    <!DOCTYPE html>
    <html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no">
        <title>注册</title>
        <link rel="stylesheet" href="/asset/plugins/mui/css/mui.css"/>
        <link rel="stylesheet" href="/asset/css/login.css"/>
        <script src="/asset/plugins/mui/js/mui.js"></script>
        <script src="/asset/js/vue.min.js"></script>
        <script src="/asset/js/util.js"></script>
    </head>
    <body>

    <header class="mui-bar mui-bar-nav">
        <h1 class="mui-title">注册</h1>
    </header>
    <div class="mui-content" id="pageapp">
        <form id='login-form' class="mui-input-group">
            <div class="mui-input-row">
                <label>账号</label>
                <input v-model="user.mobile" placeholder="请输入手机号" type="text" class="mui-input-clear mui-input">
            </div>
            <div class="mui-input-row">
                <label>密码</label>
                <input v-model="user.passwd" placeholder="请输入密码" type="password" class="mui-input-clear mui-input">
            </div>
        </form>
        <div class="mui-content-padded">
            <button @click="login" type="button" class="mui-btn mui-btn-block mui-btn-primary">注册</button>
            <div class="link-area"><a id='reg' href="register.shtml">注册账号</a> <span class="spliter">|</span> <a
                        id='forgetPassword'>忘记密码</a>
            </div>
        </div>
        <div class="mui-content-padded oauth-area">
        </div>
    </div>
    </body>
    </html>
    <script>
        var app = new Vue({
            el: "#pageapp",
            data: function () {
                return {
                    user: {
                        mobile: "",
                        passwd: ""
                    }
                }
            },
            methods: {
                login: function () {
                    //检测手机号是否正确
                    console.log("login")
                    //检测密码是否为空

                    //网络请求
                    //封装了promis
                    util.post("/user/login", this.user).then(res => {
                        console.log(res)
                        if (res.code != 0) {
                            mui.toast(res.msg)
                        } else {
                            //location.replace("//127.0.0.1/demo/index.shtml")
                            mui.toast("登录成功,即将跳转")
                        }
                    })
                },
            }
        })
    </script>
{{end}}

7.2.5.前端代码-test

{{/*定义模板的名称,shtml表示是后端渲染的文件*/}}
{{define "/user/test.shtml"}}
    <!DOCTYPE html>
    <html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no">
        <title>test</title>
        <link rel="stylesheet" href="/asset/plugins/mui/css/mui.css"/>
        <link rel="stylesheet" href="/asset/css/login.css"/>
        <script src="/asset/plugins/mui/js/mui.js"></script>
        <script src="/asset/js/vue.min.js"></script>
        <script src="/asset/js/util.js"></script>
    </head>
    <body>

    <header class="mui-bar mui-bar-nav">
        <h1 class="mui-title">test</h1>
    </header>
    <h1>test</h1>
    </body>
    </html>

{{end}}

8.在golang中使用xorm操作数据库

8.1.xorm安装

xorm的github地址https://github.com/go-xorm/xorm
xorm的中文文档地址https://github.com/go-xorm/xorm/blob/master/README_CN.md

go get -u -v github.com/go-xorm/xorm
安装xorm

8.2.MySQL驱动的安装

go的mysql驱动的github地址https://github.com/go-sql-driver/mysql

go get -u -v github.com/go-sql-driver/mysql
go的mysql驱动的安装

8.3.xorm初始化

8.4.xorm实现增删改查

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容