build-web-application-with-golang学习笔记

build-web-application-with-golang 学习教程javascript

这几周学习以上教程,仅记录一些重点难点部分。java

Go语言

Go语言基础

Go是一门相似C的编译型语言,可是它的编译速度很是快。这门语言的关键字总共也就二十五个:mysql

break    default      func    interface    select
case     defer        go      map          struct
chan     else         goto    package      switch
const    fallthrough  if      range        type
continue for          import  return       var
  • var和const参考2.2Go语言基础里面的变量和常量申明
  • package和import已经有太短暂的接触
  • func 用于定义函数和方法
  • return 用于从函数返回
  • defer 用于相似析构函数
  • go 用于并发
  • select 用于选择不一样类型的通信
  • interface 用于定义接口,参考2.6小节
  • struct 用于定义抽象数据类型,参考2.5小节
  • break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
  • chan用于channel通信
  • type用于声明自定义类型
  • map用于声明map类型数据
  • range用于读取slice、map、channel数据

Go程序是经过package来组织的,package <pkgName>这一行告诉咱们当前文件属于哪一个包,而包名main则告诉咱们它是一个可独立运行的包,它在编译后会产生可执行文件。除了main包以外,其它的包最后都会生成*.a文件(也就是包文件)并放置在$GOPATH/pkg/$GOOS_$GOARCH中(以Mac为例就是$GOPATH/pkg/darwin_amd64)。git

每个可独立运行的Go程序,一定包含一个package main,在这个main包中一定包含一个入口函数main,而这个函数既没有参数
,也没有返回值。github

包的概念和Python中的package相似,它们都有一些特别的好处:模块化(可以把你的程序分红多个模块)和可重用性(每一个模块都能被其它应用程序反复使用)。golang

Go语言重点难点

interface

简单的说,interface是一组method签名的组合,咱们经过interface来定义对象的一组行为。web

package main

import "fmt"

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名字段
    school string
    loan float32
}

type Employee struct {
    Human //匿名字段
    company string
    money float32
}

//Human实现SayHi方法
func (h Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

//Human实现Sing方法
func (h Human) Sing(lyrics string) {
    fmt.Println("La la la la...", lyrics)
}

//Employee重载Human的SayHi方法
func (e Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone)
    }

// Interface Men被Human,Student和Employee实现
// 由于这三个类型都实现了这两个方法
type Men interface {
    SayHi()
    Sing(lyrics string)
}

func main() {
    mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}

    //定义Men类型的变量i
    var i Men

    //i能存储Student
    i = mike
    fmt.Println("This is Mike, a Student:")
    i.SayHi()
    i.Sing("November rain")

    //i也能存储Employee
    i = tom
    fmt.Println("This is tom, an Employee:")
    i.SayHi()
    i.Sing("Born to be wild")

    //定义了slice Men
    fmt.Println("Let's use a slice of Men and see what happens")
    x := make([]Men, 3)
    //这三个都是不一样类型的元素,可是他们实现了interface同一个接口
    x[0], x[1], x[2] = paul, sam, mike

    for _, value := range x{
        value.SayHi()
    }
}

interface就是一组抽象方法的集合,它必须由其余非interface类型实现,而不能自我实现。sql

空interface

空interface(interface{})不包含任何的method,正由于如此,全部的类型都实现了空interface。空interface对于描述起不到任何的做用(由于它不包含任何的method),可是空interface在咱们须要存储任意类型的数值的时候至关有用,由于它能够存储任意类型的数值。它有点相似于C语言的void*类型。shell

// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a能够存储任意类型的数值
a = i
a = s

goroutine

goroutine是Go并行设计的核心。goroutine说到底其实就是协程,可是它比线程更小,十几个goroutine可能体如今底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),固然会根据相应的数据伸缩。也正由于如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。数据库

goroutine是经过Go的runtime管理的一个线程管理器。goroutine经过go关键字实现了,其实就是一个普通的函数。

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world") //开一个新的Goroutines执行
    say("hello") //当前Goroutines执行
}

// 以上程序执行后将输出:
// hello
// world
// hello
// world
// hello
// world
// hello
// world
// hello

channels

goroutine运行在相同的地址空间,所以访问共享内存必须作好同步。那么goroutine之间如何进行数据的通讯呢,Go提供了一个很好的通讯机制channel。channel能够与Unix shell 中的双向管道作类比:能够经过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也须要定义发送到channel的值的类型。注意,必须使用make 建立channel:

package main

import "fmt"

func sum(a []int, c chan int) {
    total := 0
    for _, v := range a {
        total += v
    }
    c <- total  // send total to c
}

func main() {
    a := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(a[:len(a)/2], c)
    go sum(a[len(a)/2:], c)
    x, y := <-c, <-c  // receive from c

    fmt.Println(x, y, x + y)
}

Web Service With Golang

Go搭建一个Web服务器

Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,经过http包能够很方便的就搭建起来一个能够运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操做。

package main

import (
    "fmt"
    "net/http"
    "strings"
    "log"
)

func sayhelloName(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()  //解析参数,默认是不会解析的
    fmt.Println(r.Form)  //这些信息是输出到服务器端的打印信息
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"])
    for k, v := range r.Form {
        fmt.Println("key:", k)
        fmt.Println("val:", strings.Join(v, ""))
    }
    fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的
}

func main() {
    http.HandleFunc("/", sayhelloName) //设置访问的路由
    err := http.ListenAndServe(":9090", nil) //设置监听的端口
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

看到上面这个代码,要编写一个Web服务器很简单,只要调用http包的两个函数就能够了(相似于Python的tornado)。咱们build以后,而后执行web.exe,这个时候其实已经在9090端口监听http连接请求了。

在浏览器输入http://localhost:9090

能够看到浏览器页面输出了Hello astaxie!

能够换一个地址试试:http://localhost:9090/?url_long=111&url_long=222

看看浏览器输出的是什么,服务器输出的是什么?

用户访问Web以后服务器端打印的信息

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是经过一个函数ListenAndServe来处理这些事情的,这个底层其实这样处理的:初始化一个server对象,而后调用了net.Listen("tcp", addr),也就是底层用TCP协议搭建了一个服务,而后监控咱们设置的端口。

下面代码来自Go的http包的源码,经过下面的代码咱们能够看到整个的http处理过程:

func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    var tempDelay time.Duration // how long to sleep on accept failure
    for {
        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
        c, err := srv.newConn(rw)
        if err != nil {
            continue
        }
        go c.serve()
    }
}

监控以后如何接收客户端的请求呢?上面代码执行监控端口以后,调用了srv.Serve(net.Listener)函数,这个函数就是处理接收客户端的请求信息。这个函数里面起了一个for{},首先经过Listener接收请求,其次建立一个Conn,最后单独开了一个goroutine,把这个请求的数据当作参数扔给这个conn去服务:go c.serve()。这个就是高并发体现了,用户的每一次请求都是在一个新的goroutine去服务,相互不影响。

那么如何具体分配到相应的函数来处理请求呢?conn首先会解析request:c.readRequest(),而后获取相应的handler:handler := c.server.Handler,也就是咱们刚才在调用函数ListenAndServe时候的第二个参数,咱们前面例子传递的是nil,也就是为空,那么默认获取handler = DefaultServeMux,那么这个变量用来作什么的呢?对,这个变量就是一个路由器,它用来匹配url跳转到其相应的handle函数,那么这个咱们有设置过吗?有,咱们调用的代码里面第一句不是调用了http.HandleFunc("/", sayhelloName)嘛。这个做用就是注册了请求/的路由规则,当请求uri为"/",路由就会转到函数sayhelloName,DefaultServeMux会调用ServeHTTP方法,这个方法内部其实就是调用sayhelloName自己,最后经过写入response的信息反馈到客户端。

详细的整个流程以下图所示:
一个http链接处理流程

Go代码的执行流程

经过对http包的分析以后,如今让咱们来梳理一下整个的代码执行过程。

  • 首先调用Http.HandleFunc

    按顺序作了几件事:

    1 调用了DefaultServeMux的HandleFunc

    2 调用了DefaultServeMux的Handle

    3 往DefaultServeMux的map[string]muxEntry中增长对应的handler和路由规则

  • 其次调用http.ListenAndServe(":9090", 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的ServeHTTP

表单

表单是咱们日常编写Web应用经常使用的工具,经过表单咱们能够方便的让客户端和服务器进行数据的交互。
表单是一个包含表单元素的区域。表单元素是容许用户在表单中(好比:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(

)定义。

处理表单输入

默认状况下,Handler里面是不会自动解析form的,必须显式的调用r.ParseForm()后,你才能对这个表单数据进行操做。

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"])

验证表单输入

咱们日常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有不少的插件库,好比ValidationJS插件),一个是在服务器端的验证,这里关注如何在服务器端验证。

处理文件上传

在服务器端,咱们增长一个handlerFunc:

http.HandleFunc("/upload", upload)

// 处理/upload 逻辑
func upload(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("upload.gtpl")
        t.Execute(w, token)
    } else {
        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)
        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)
    }
}

经过上面的代码能够看到,处理文件上传咱们须要调用r.ParseMultipartForm,里面的参数表示maxMemory,调用ParseMultipartForm以后,上传的文件存储在maxMemory大小的内存里面,若是文件大小超过了maxMemory,那么剩下的部分将存储在系统的临时文件中。咱们能够经过r.FormFile获取上面的文件句柄,而后实例中使用了io.Copy来存储文件。

获取其余非文件字段信息的时候就不须要调用r.ParseForm,由于在须要的时候Go自动会去调用。并且ParseMultipartForm调用一次以后,后面再次调用不会再有效果。

经过上面的实例咱们能够看到咱们上传文件主要三步处理:

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

访问数据库

使用MySQL数据库

Go中支持MySQL的驱动目前比较多,有以下几种,有些是支持database/sql标准,而有些是采用了本身的实现接口,经常使用的有以下几种:

  • 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,保持长链接,虽然星星fork的mymysql也支持keepalive,但不是线程安全的,这个从底层就支持了keepalive。

示例代码

接下来的几个小节里面咱们都将采用同一个数据库表结构:数据库test,用户表userinfo,关联用户信息表userdetail。

CREATE TABLE `userinfo` (
    `uid` INT(10) NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(64) NULL DEFAULT NULL,
    `departname` 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接口对数据库表进行增删改查操做

package main

import (
    "database/sql"
    "fmt"
    //"time"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8")
    checkErr(err)

    //插入数据
    stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?")
    checkErr(err)

    res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
    checkErr(err)

    id, err := res.LastInsertId()
    checkErr(err)

    fmt.Println(id)
    //更新数据
    stmt, err = db.Prepare("update userinfo set username=? where uid=?")
    checkErr(err)

    res, err = stmt.Exec("astaxieupdate", id)
    checkErr(err)

    affect, err := res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    //查询数据
    rows, err := db.Query("SELECT * FROM userinfo")
    checkErr(err)

    for rows.Next() {
        var uid int
        var username string
        var department string
        var created string
        err = rows.Scan(&uid, &username, &department, &created)
        checkErr(err)
        fmt.Println(uid)
        fmt.Println(username)
        fmt.Println(department)
        fmt.Println(created)
    }

    //删除数据
    stmt, err = db.Prepare("delete from userinfo where uid=?")
    checkErr(err)

    res, err = stmt.Exec(id)
    checkErr(err)

    affect, err = res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    db.Close()

}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

经过上面的代码咱们能够看出,Go操做Mysql数据库是很方便的。

关键的几个函数我解释一下:

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.Query()函数用来直接执行Sql返回Rows结果。

stmt.Exec()函数用来执行stmt准备好的SQL语句

咱们能够看到咱们传入的参数都是=?对应的数据,这样作的方式能够必定程度上防止SQL注入。

使用Beego orm库进行ORM开发

beego orm是我开发的一个Go进行ORM操做的库,它采用了Go style方式对数据库进行操做,实现了struct到数据表记录的映射。beego orm是一个十分轻量级的Go ORM框架,开发这个库的本意下降复杂的ORM学习曲线,尽量在ORM的运行效率和功能之间寻求一个平衡,beego orm是目前开源的Go ORM框架中实现比较完整的一个库,并且运行效率至关不错,功能也基本能知足需求。

beego orm是支持database/sql标准接口的ORM库,因此理论上来讲,只要数据库驱动支持database/sql接口就能够无缝的接入beego orm。目前我测试过的驱动包括下面几个:

Mysql: github/go-mysql-driver/mysql

PostgreSQL: github.com/lib/pq

SQLite: github.com/mattn/go-sqlite3

Mysql: github.com/ziutek/mymysql/godrv

首先你须要import相应的数据库驱动包、database/sql标准接口包以及beego orm包,以下所示:

import (
    "database/sql"
    "github.com/astaxie/beego/orm"
    _ "github.com/go-sql-driver/mysql"
)

func init() {
    //注册驱动
    orm.RegisterDriver("mysql", orm.DR_MySQL)
    //设置默认数据库
    orm.RegisterDataBase("default", "mysql", "root:root@/my_db?charset=utf8", 30)
    //注册定义的model
        orm.RegisterModel(new(User))

    // 建立table
        orm.RunSyncdb("default", false, true)
}

导入必须的package以后,咱们须要打开到数据库的连接,而后建立一个beego orm对象(以MySQL为例),以下所示
beego orm:

func main() {
        o := orm.NewOrm()
}

简单示例:

package main

import (
    "fmt"
    "github.com/astaxie/beego/orm"
    _ "github.com/go-sql-driver/mysql" // 导入数据库驱动
)

// Model Struct
type User struct {
    Id   int
    Name string `orm:"size(100)"`
}

func init() {
    // 设置默认数据库
    orm.RegisterDataBase("default", "mysql", "root:root@/my_db?charset=utf8", 30)
    
    // 注册定义的 model
    orm.RegisterModel(new(User))
//RegisterModel 也能够同时注册多个 model
//orm.RegisterModel(new(User), new(Profile), new(Post))

    // 建立 table
    orm.RunSyncdb("default", false, true)
}

func main() {
    o := orm.NewOrm()

    user := User{Name: "slene"}

    // 插入表
    id, err := o.Insert(&user)
    fmt.Printf("ID: %d, ERR: %v\n", id, err)

    // 更新表
    user.Name = "astaxie"
    num, err := o.Update(&user)
    fmt.Printf("NUM: %d, ERR: %v\n", num, err)

    // 读取 one
    u := User{Id: user.Id}
    err = o.Read(&u)
    fmt.Printf("ERR: %v\n", err)

    // 删除表
    num, err = o.Delete(&u)
    fmt.Printf("NUM: %d, ERR: %v\n", num, err)
}

session和数据存储

session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不一样。session经过cookie,在客户端保存session id,而将用户的其余会话消息保存在服务端的session对象中,与此相对的,cookie须要将全部信息都保存在客户端。所以cookie存在着必定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其余网站收集(例如:1. appA主动设置域B cookie,让域B cookie获取;2. XSS,在appA上经过javascript获取document.cookie,并传递给本身的appB)。

session建立过程

session的基本原理是由服务器为每一个会话维护一份信息数据,客户端和服务端依靠一个全局惟一的标识来访问这份数据,以达到交互的目的。当用户访问Web应用时,服务端程序会随须要建立session,这个过程能够归纳为三个步骤:

  • 生成全局惟一标识符(sessionid);
  • 开辟数据存储空间。通常会在内存中建立相应的数据结构,但这种状况下,系统一旦掉电,全部的会话数据就会丢失,若是是电子商务类网站,这将形成严重的后果。因此为了解决这类问题,你能够将会话数据写到文件里或存储在数据库中,固然这样会增长I/O开销,可是它能够实现某种程度的session持久化,也更有利于session的共享;
  • 将session的全局惟一标示符发送给客户端。

以上三个步骤中,最关键的是如何发送这个session的惟一标识这一步上。考虑到HTTP协议的定义,数据无非能够放到请求行、头域或Body里,因此通常来讲会有两种经常使用的方式:cookie和URL重写。

  1. Cookie
    服务端经过设置Set-cookie头就能够将session的标识符传送到客户端,而客户端此后的每一次请求都会带上这个标识符,另一般包含session信息的cookie会将失效时间设置为0(会话cookie),即浏览器进程有效时间。至于浏览器怎么处理这个0,每一个浏览器都有本身的方案,但差异都不会太大(通常体如今新建浏览器窗口的时候);
  2. URL重写
    所谓URL重写,就是在返回给用户的页面里的全部的URL后面追加session标识符,这样用户在收到响应以后,不管点击响应页面里的哪一个连接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。虽然这种作法比较麻烦,可是,若是客户端禁用了cookie的话,此种方案将会是首选。
package memory

import (
    "container/list"
    "github.com/astaxie/session"
    "sync"
    "time"
)

var pder = &Provider{list: list.New()}

type SessionStore struct {
    sid          string                      //session id惟一标示
    timeAccessed time.Time                   //最后访问时间
    value        map[interface{}]interface{} //session里面存储的值
}

func (st *SessionStore) Set(key, value interface{}) error {
    st.value[key] = value
    pder.SessionUpdate(st.sid)
    return nil
}

func (st *SessionStore) Get(key interface{}) interface{} {
    pder.SessionUpdate(st.sid)
    if v, ok := st.value[key]; ok {
        return v
    } else {
        return nil
    }
}

func (st *SessionStore) Delete(key interface{}) error {
    delete(st.value, key)
    pder.SessionUpdate(st.sid)
    return nil
}

func (st *SessionStore) SessionID() string {
    return st.sid
}

type Provider struct {
    lock     sync.Mutex               //用来锁
    sessions map[string]*list.Element //用来存储在内存
    list     *list.List               //用来作gc
}

func (pder *Provider) SessionInit(sid string) (session.Session, error) {
    pder.lock.Lock()
    defer pder.lock.Unlock()
    v := make(map[interface{}]interface{}, 0)
    newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
    element := pder.list.PushBack(newsess)
    pder.sessions[sid] = element
    return newsess, nil
}

func (pder *Provider) SessionRead(sid string) (session.Session, error) {
    if element, ok := pder.sessions[sid]; ok {
        return element.Value.(*SessionStore), nil
    } else {
        sess, err := pder.SessionInit(sid)
        return sess, err
    }
    return nil, nil
}

func (pder *Provider) SessionDestroy(sid string) error {
    if element, ok := pder.sessions[sid]; ok {
        delete(pder.sessions, sid)
        pder.list.Remove(element)
        return nil
    }
    return nil
}

func (pder *Provider) SessionGC(maxlifetime int64) {
    pder.lock.Lock()
    defer pder.lock.Unlock()

    for {
        element := pder.list.Back()
        if element == nil {
            break
        }
        if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
            pder.list.Remove(element)
            delete(pder.sessions, element.Value.(*SessionStore).sid)
        } else {
            break
        }
    }
}

func (pder *Provider) SessionUpdate(sid string) error {
    pder.lock.Lock()
    defer pder.lock.Unlock()
    if element, ok := pder.sessions[sid]; ok {
        element.Value.(*SessionStore).timeAccessed = time.Now()
        pder.list.MoveToFront(element)
        return nil
    }
    return nil
}

func init() {
    pder.sessions = make(map[string]*list.Element, 0)
    session.Register("memory", pder)
}

Web服务

错误处理,调试和测试

部署与维护

如何设计一个Web框架

扩展Web框架

相关文章
相关标签/搜索