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框架

相關文章
相關標籤/搜索