Golang在搭建web服务器方面的能力是毋庸置疑的。官方已经有提供net/http包为搭建http服务器做准备。使用这个包能很简单地对web的路由,静态文件,模版,cookie等数据进行设置。至于这个包是否好用,这个就见仁见智了。你可以从net包开始封装一个web框架,当然也可以基于http包封装一个web框架。但是不论你是打算怎么样做,了解基本的net/http包一定是你借鉴的基础。
需求
我们要做两个简单的后台web系统。这个系统简单到只有两个页面:登陆和首页。
1 登陆页面
登陆页面需要提交用户名和密码,将用户名和密码和mysql数据库中的用户名密码比对达到验证的效果。mysql数据库的go语言驱动推荐使用mymysql(https://github.com/ziutek/mymysql)。
当用户名和密码正确的时候,需要在cookie中种下用户名和加密后的密钥来进行cookie认证。我们不对cookie设置ExpireTime,这样这个cookie的有效期就是浏览器打开到浏览器关闭的session期间。
另外,这个页面还需要加载一个js。提交用户名和密码的是由js进行ajax post命令进行查询的。
这个页面需要加载css,进行页面排版
2 首页
首页是非常简单,但它是一个动态页面。
首先右上角的”欢迎登陆, 管理员:yejianfeng“。这里的用户名yejianfeng是根据不同的用户会进行变化的。这里需要用到模版,我们又会用到了一个模版包html/template。这个包的作用就是加载模版。
其次这个页面也需要的是css,js(退出系统的删除cookie操作由js控制)
路由
分析下访问路径,会有几个文件:
/admin/index -- 首页
/login/index --登陆页显示
/ajax/login -- 登陆的ajax动作
/css/main.css -- 获取main的css文件
/js/login.js -- 获取登陆的js文件
在net/http包中,动态文件的路由和静态文件的路由是分开的,动态文件使用http.HandleFunc进行设置,静态文件就需要使用到http.FileServer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package
main import
( "net/http" ) func
main() { http.Handle( "/css/" ,
http.FileServer(http.Dir( "template" ))) http.Handle( "/js/" ,
http.FileServer(http.Dir( "template" ))) http.HandleFunc( "/admin/" ,
adminHandler) http.HandleFunc( "/login/" ,loginHandler) http.HandleFunc( "/ajax/" ,ajaxHandler) http.HandleFunc( "/" ,NotFoundHandler) http.ListenAndServe( ":8888" ,
nil) } |
这里的http.FileServer(http.Dir("template"))的路径是怎么算的要注意下了。相对路径都是从当前执行路径路径下开始算的,这里设置的路径树是这样:
关于http.HandleFunc如果不理解请参考我的上一篇文章
http://www.cnblogs.com/yjf512/archive/2012/08/22/2650873.html
处理器
这里需要定制4个handler对应相应的一级路径。我们将这些个handler都放入到route.go文件中
页面404处理器
main中的逻辑是当/admin/ /login/ /ajax/都不符合路径的时候就进入404页面处理器NotFoundHandler
1
2
3
4
5
6
7
8
9
10
11
12
|
func
NotFoundHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path
== "/" { http.Redirect(w,
r, "/login/index" ,
http.StatusFound) } t,
err := template.ParseFiles( "template/html/404.html" ) if (err
!= nil) { log.Println(err) } t.Execute(w,
nil) } |
这段逻辑是很清晰的:如果路径是"/" 即访问路径是http://192.168.100.166:8888/ 的时候,访问会跳转到登陆页面,否则当访问路径不满足制定的路由,读取404模版,显示404。
强烈推荐使用template.ParseFile模版文件解析
template包中也提供了Parse这类直接在代码中写上模版的函数。使用起来,你会发现这类函数远没有ParseFile好用,原因很明显,如果你把模版写在代码中,一旦模版需要进行小细节的修改,也需要重新编译。并且我们使用模版的目的就是将显示逻辑和业务逻辑分离,Parse会导致整个代码是不可维护!
当然有人会考虑到效率问题,一个是读取文件,一个是直接读取内存。但是我觉得这点效率差别用户根本不会察觉到,牺牲极小的性能保证工程性是很值得的。
ParseFile中的路径问题也是很容易犯的问题。这里的填写相对路径,则文件是从当前执行的路径开始算。
比如这个路径,执行bin/webdemo,template/html/404.html就对应/go/gopath/template/html/404.html
这一步后续动作是在template/html文件夹中创建404.html页面
登陆页面处理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func
loginHandler(w http.ResponseWriter, r *http.Request) { pathInfo
:= strings.Trim(r.URL.Path, "/" ) parts
:= strings.Split(pathInfo, "/" ) var action
= "" if len(parts)
> 1 { action
= strings.Title(parts[1]) + "Action" } login
:= &loginController{} controller
:= reflect.ValueOf(login) method
:= controller.MethodByName(action) if !method.IsValid()
{ method
= controller.MethodByName(strings.Title( "index" )
+ "Action" ) } requestValue
:= reflect.ValueOf(r) responseValue
:= reflect.ValueOf(w) method.Call([]reflect.Value{responseValue,
requestValue}) } |
根据MVC思想,对具体的逻辑内容使用不同的Controller,这里定义了一个loginController, 使用reflect将pathInfo映射成为controller中的方法。
这样做的好处显而易见:
1 清晰的文件逻辑。不同的一级目录分配到不同的控制器,不同的控制器中有不同的方法处理。这些控制器放在一个独立的文件中,目录结构清晰干净。
2 路由和业务逻辑分开。 main中的http.HandleFunc处理一级路由,route处理二级路由,controller处理业务逻辑,每个单元分开处理。
好了,下面需要定义loginContrller,我们另外起一个文件loginController.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package
main import
( "net/http" "html/template" ) type
loginController struct { } func
( this *loginController)IndexAction(w
http.ResponseWriter, r *http.Request) { t,
err := template.ParseFiles( "template/html/login/index.html" ) if (err
!= nil) { log.Println(err) } t.Execute(w,
nil) } |
下面需要创建template/html/login/index.html
这个文件中包含mian.css, jquery.js, base.js。 创建这些css和js。具体的css和js内容请看github源码
js中的逻辑是这样写的:当login表单提交的时候,会调用/ajax/login进行验证操作,下面就开始写ajax的处理器
ajax处理器
route中的ajaxHandler和loginHandler是大同小异,这里就忽略不说了,具体说一下ajaxController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
package
main import
( "net/http" "github.com/ziutek/mymysql/autorc" _
"github.com/ziutek/mymysql/thrsafe" "encoding/json" ) type
Result struct { Ret
int Reason
string Data
interface {} } type
ajaxController struct { } func
( this *ajacController)LoginAction(w
http.ResponseWriter, r *http.Request) { w.Header().Set( "content-type" ,
"application/json" ) err
:= r.ParseForm() if err
!= nil { OutputJson(w,
0, "参数错误" ,
nil) return } admin_name
:= r.FormValue( "admin_name" ) admin_password
:= r.FormValue( "admin_password" ) if admin_name
== "" ||
admin_password == "" { OutputJson(w,
0, "参数错误" ,
nil) return } db
:= mysql.New( "tcp" ,
"" ,
"192.168.199.128" ,
"root" ,
"test" ,
"webdemo" ) if err
:= db.Connect(); err != nil { OutputJson(w,
0, "数据库操作失败" ,
nil) return } defer
db.Close() rows,
res, err := db.Query( "select
* from webdemo_admin where admin_name = '%s'" ,
admin_name) if err
!= nil { OutputJson(w,
0, "数据库操作失败" ,
nil) return } name
:= res.Map( "admin_password" ) admin_password_db
:= rows[0].Str(name) if admin_password_db
!= admin_password { OutputJson(w,
0, "密码输入错误" ,
nil) return } //
存入cookie,使用cookie存储 expiration
:= time.Unix(1, 0) cookie
:= http.Cookie{Name: "admin_name" ,
Value: rows[0].Str(res.Map( "admin_name" )),
Path: "/" } http.SetCookie(w,
&cookie) OutputJson(w,
1, "操作成功" ,
nil) return } func
OutputJson(w http.ResponseWriter, ret int ,
reason string ,
i interface {})
{ out :=
&Result{ret, reason, i} b,
err := json.Marshal( out ) if err
!= nil { return } w.Write(b) } |
这段代码有几个地方可以看看:
如何设置header:
w.Header().Set("content-type", "application/json")
如何解析参数:
err := r.ParseForm()
admin_name := r.FormValue("admin_name")
如何连接数据库
db := mysql.New("tcp", "", "192.168.199.128", "root", "test", "webdemo")
当然这里得需要有个数据库和数据表,建表和建表的sql文件在github上能看到
create table webdemo_admin
(
admin_id int not null auto_increment,
admin_name varchar(32) not null,
admin_password varchar(32) not null,
primary key(admin_id)
);
如何设置cookie
cookie := http.Cookie{Name: "admin_name", Value: rows[0].Str(res.Map("admin_name")), Path: "/"}
http.SetCookie(w, &cookie)
主页处理器
adminHandler的逻辑比其他Handler多的一个是需要 获取cookie
cookie, err := r.Cookie("admin_name")
并且在传递给controller的时候需要将admin_name传递进去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func
adminHandler(w http.ResponseWriter, r *http.Request) { //
获取cookie cookie,
err := r.Cookie( "admin_name" ) if err
!= nil || cookie.Value == "" { http.Redirect(w,
r, "/login/index" ,
http.StatusFound) } pathInfo
:= strings.Trim(r.URL.Path, "/" ) parts
:= strings.Split(pathInfo, "/" ) admin
:= &adminController{} controller
:= reflect.ValueOf(admin) method
:= controller.MethodByName(action) if !method.IsValid()
{ method
= controller.MethodByName(strings.Title( "index" )
+ "Action" ) } requestValue
:= reflect.ValueOf(r) responseValue
:= reflect.ValueOf(w) userValue
:= reflect.ValueOf(cookie.Value) method.Call([]reflect.Value{responseValue,
requestValue, Uservalue}) } |
其他的部分都是一样的了。
它对应的Controller的Action是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
type
User struct { UserName
string } type
adminController struct { } func
( this *adminController)IndexAction(w
http.ResponseWriter, r *http.Request, user string )
{ t,
err := template.ParseFiles( "template/html/admin/index.html" ) if (err
!= nil) { log.Println(err) } t.Execute(w,
&User{user}) } |
这里就将user传递出去给admin/index模版
模版内部使用{{.UserName}}进行参数显示
后记
至此,这个基本的webdemo就完成了。启动服务之后,就会在8888端口开启了web服务。
当然,这个web服务还有许多东西可以优化,个人信息验证,公共模板的提炼使用,数据库的密码使用密文等。但是这个例子中已经用到了搭建web服务器最基本的几个技能了。
有疑问加站长微信联系(非本文作者)