Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Web開發基礎

第2章 Go Web開發基礎

2.1 helloWorldWeb

//helloWorldWeb.go
//go run helloWorldWeb.go
//127.0.0.1
package main
import (
	"fmt"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World")
}

func main() {
	server := &http.Server {
		Addr: "0.0.0.0:80",
	}
	http.HandleFunc("/", hello)
	server.ListenAndServe()
}

2.2 Web程序運行原理簡介

2.2.1 Web基本原理

  1. 運行原理 (1)用戶打開客戶端瀏覽器,輸入URL地址。 (2)客戶端瀏覽器通過HTTP協議向服務器端發送瀏覽請求。 (3)服務器端通過CGI程序接收請求,調用解釋引擎處理“動態內容”,訪問數據庫並處理數據,通過HTTP協議將得到的處理結果返回給客戶端瀏覽器。 (4)客戶端瀏覽器解釋並顯示HTML頁面。
  2. DNS(Domain Name System,域名系統) 將主機名和域名轉換為IP地址。 DNS解析過程: (1)用戶打開瀏覽器,輸入URL地址。瀏覽器從URL中抽取域名(主機名),傳給DNS應用程序的客戶端。 (2)DNS客戶端向DNS服務器端發送查詢報文,其中包含主機名。 (3)DNS服務器端向DNS客戶端發送回答報文,其中包含該主機名對應IP地址。 (4)瀏覽器收到DNS的IP地址後,向該IP地址定位的HTTP服務器端發起TCP連接。

2.2.2 HTTP簡介

HTTP(Hyper Text Transfer Protocal,超文本傳輸協議),簡單請求-響應協議,運行在TCP協議上,無狀態。它指定客戶端發送給服務器端的消息和得到的響應。請求和響應消息頭是ASCII碼;消息內容則類似MIME格式。

2.2.3 HTTP請求

客戶端發送到服務器端的請求消息。

  1. 請求行(Request Line)

請求方法、URI、HTTP協議/協議版本組成。

請求方法方法描述
GET請求頁面,並返回頁面內容,請求參數包含在URL中,提交數據最多1024byte
HEAD類似GET,只獲取報頭
POST提交表單或上傳文件,數據(含請求參數)包含在請求體中
PUT取代指定內容的文檔
DELETE刪除指定資源
OPTIONS查看服務器的性能
CONNECT服務器當作跳板,訪問其他網頁
TRACE回顯服務器收到的請求,用於測試或診斷
  1. 請求頭(Request Header)
請求頭示例說明
AcceptAccept: text/plain, text/html客戶端能夠接收的內容類型
Accept-charsetAccept-charset: iso-8859-5字符編碼集
Accept-EncodingAccept-Encoding: compress, gzip壓縮編碼類型
Accept-LanguageAccept-Language: en, zh語言
Accept-RangesAccept-Ranges: bytes子範圍字段
AuthorizationAuthorization: Basic dbXleoOEpePOetpoe2Ftyd==授權證書
Cache-ControlCache-Control: no-cache緩存機制
ConnectionConnection: close是否需要持久連接(HTTP1.1默認持久連接)
CookieCookie: $version=1; Skin=new;請求域名下的所有cookie值
Content-LengthContent-Length: 348內容長度
  1. 請求體(Request Body)

HTTP請求中傳輸數據的實體。

2.2.4 HTTP響應

服務器端返回給客戶端。

  1. 響應狀態碼(Response Status Code)

表示服務器的響應狀態。

狀態碼說明詳情
100繼續服務器收到部分請求,等待客戶端繼續提出請求
101切換協議請求者已要求服務器切換協議,服務器已確認並準備切換協議
200成功成功處理請求
201已創建服務器創建了新的資源
202已接受已接收請求,但尚未處理
203非授權信息成功處理請求,但返回信息來自另一個源
204無內容成功處理請求,無返回內容
205重置內容成功處理請求,內容重置
206部分內容成功處理部分內容
300多種選擇可執行多種操作
301永久移動永久重定向
302臨時移動暫時重定向
303查看其他位置重定向目標文檔應通過GET獲取
304未修改使用上次網頁資源
305使用代理應使用代理訪問
307臨時重定向臨時從其他位置響應
400錯誤請求無法解析
401未授權無身份驗證或驗證未通過
403禁止訪問拒絕
404未找到找不到
405方法禁用禁用指定方法
406不接受無法使用內容響應
407需要代理授權需要使用代理授權
408請求超時請求超時
409沖突完成請求時發生沖突
410已刪除資源永久刪除
411需要有效長度不接受標頭字段不含有效內容長度
412未滿足前提條件服務器未滿足某個前提條件
413請求實體過大超出能力
414請求URI過長網址過長,無法處理
415不支持類型格式不支持
416請求範圍不符頁面無法提供請求範圍
417未滿足期望值未滿足期望請求標頭字段
500服務器內部發生錯誤服務器錯誤
501未實現不具備功能
502錯誤網關收到無效響應
503服務不可用無法使用
504網關超時沒及時收到請求
505HTTP版本不支持不支持HTTP協議版本
  1. 響應頭(Response Headers)

包含服務器對請求的應答信息。

響應頭說明
Allow服務器支持的請求方法
Content-Encondig文檔編碼方法。
Content-Length內容長度,瀏覽器持久HTTP連接時需要
Content-Type文檔的MIME類型
DateGMT時間
Expires過期時間後,不再緩存
Last-Modified文檔最後改動時間。通過比較客戶端頭if-Modified-Since,可能返回304(Not Modified)。
Location客戶端應去哪裡提取文檔。
Refresh瀏覽器應刷新時間,秒
Server服務器名字
Set-Cookie設置頁面關聯Cookie
WWW-Authenticate客戶應在Authorization中提供授權信息,通常返回401。
  1. 響應體(Response Body)

HTTP請求返回的內容。 HTML,二進制數據,JSON文檔,XML文檔等。

2.2.5 URI與URL

  1. URI(Uniform Resource Identifier,統一資源標識符) 用來標識Web上每一種可用資源,概念。由資源的命名機制、存放資源的主機名、資源自身的名稱等組成。
  2. URL(Uniform Resource Locator,統一資源定位符) 用於描述網絡上的資源(描述信息資源的字符串),實現。使用統一格式,包括文件、服務器地址和目錄等。
scheme://host[:port#]/path/.../[?query-string][#anchor]
//協議(服務方式)
//主機域名或IP地址(可含端口號)
//具體地址,目錄和文件名等
  1. URN(Uniform Resource Name,統一資源名) 帶有名字的因特網資源,是URL的更新形式,不依賴位置,可減少失效鏈接個數。

2.2.6 HTTPS簡介

HTTPS(Hyper Text Transfer Protocol over SecureSocket Layer),在HTTP基礎上,通過傳輸加密和身份認證保證傳輸過程的安全型。HTTP + SSL/TLS。

TLS(Transport Layer Security,傳輸層安全性協議),及其前身SSL(Secure Socket Layer,安全套接字層),保障通信安全和數據完整性。

2.2.7 HTTP2簡介

  1. HTTP協議歷史
  • HTTP 0.9 只支持GET方法,不支持MIME類型和HTTP各種頭信息等。
  • HTTP 1.0 增加很多方法、各種HTTP頭信息,以及對多媒體對象的處理。
  • HTTP 1.1 主流HTTP協議,改善結構性缺陷,明確語義,增刪特性,支持更復雜的Web應用程序。
  • HTTP 2 優化性能,兼容HTTP 1.1語義,是二進制協議,頭部採用HPACK壓縮,支持多路復用、服務器推送等。
  1. HTTP 1.1與HTTP 2的對比
  • 頭信息壓縮 HTTP 1.1中,每一次發送和響應,都有HTTP頭信息。HTTP 2壓縮頭信息,減少帶寬。
  • 推送功能 HTTP 2之前,只能客戶端發送數據,服務器端返回數據。HTTP2中,服務器可以主動向客戶端發起一些數據傳輸(如css和png等),服務器可以並行發送html,css,js等數據。

2.2.8 Web應用程序的組成

  1. 處理器(hendler) 接收HTTP請求並處理。調用模板引擎生成html文檔返給客戶端。

MVC軟件架構模型

  • 模型(Model) 處理與業務邏輯相關的數據,以及封裝對數據的處理方法。有對數據直接訪問的權力,例如訪問數據庫。
  • 視圖(View) 實現有目的的顯示數據,一般沒有程序的邏輯。
  • 控制器(Controller) 組織不同層面,控制流程,處理用戶請求,模型交互等事件,並做出響應。

title模型Model控制器Controller視圖View瀏覽器模板引擎數據庫

  1. 模板引擎(template engine) 分離界面與數據(內容),組合模板(template)與數據(data),生成html文檔。 分為置換型(模板內容中特定標記替換)、解釋型和編譯型等。

模板template數據data模板引擎HTML文檔

2.3 net/http包

2.3.1 創建簡單服務器端

  1. 創建和解析HTTP服務器端
package main

import (
	"net/http"
)

func sayHello(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("Hello World"))
}

func main() {
	//註冊路由
	http.HandleFunc("/hello", sayHello)
	//開啟對客戶端的監聽
	http.ListenAndServe(":8080", nil)
}
http.HandleFunc()函數

//輸入參數:監聽端口號和事件處理器handler
http.ListenAndServe()函數

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}
package main

import (
	"net/http"
)

type Refer struct {
	handler http.Handler
	refer string
}

func (this *Refer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Referer() == this.refer {
		this.handler.ServeHTTP(w, r)
	} else {
		w.WriteHeader(403)
	}
}

func myHandler(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("this is handler"))
}

func hello(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("hello"))
}

func main() {
	referer := &Refer{
		handler: http.HandlerFunc(myHandler),
		refer: "www.shirdon.com",
	}
	http.HandleFunc("/hello", hello)
	http.ListenAndServe(":8080", referer)
}
  1. 創建和解析HTTPS服務器端
//證書文件路徑,私鑰文件路徑
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error
package main

import (
	"log"
	"net/http"
)

func handle(w http.ResponseWriter, r *http.Request) {
	log.Printf("Got connection: %s", r.Proto)
	w.Write([]byte("Hello this is a HTTP 2 message!"))
}

func main() {
	srv := &http.Server{Addr: ":8088", Handler: http.HandlerFunc(handle)}
	log.Printf("Serving on https://0.0.0.0:8088")
	log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))
}

2.3.2 創建簡單的客戶端

//src/net/http/client.go
var DefaultClient = &Client{}

func Get(url string) (resp *Response, err error) {
	return DefaultClient.Get(url)
}

func (c *Client) Get(url string) (resp *Response, err error) {
	req, err := NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	return DefaultClient.Post(url, contentType, body)
}

func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	req, err := NewRequest("POST", url, body)
	if err != nil {
		return nil, err
	}
	req.Header.set("Content-Type", contentType)
	return c.Do(req)
}
func NewRequest(method, url string, body io.Reader) (*Request, error)
//請求類型
//請求地址
//若body實現io.Closer接口,則Request返回值的Body字段會被設置為body值,並被Client的Do()、Post()和PostForm()方法關閉。
  1. 創建GET請求
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	resp, err := http.Get("https://www.baidu.com")
	if err != nil {
		fmt.Println("err:", err)
	}
	closer := resp.Body
	bytes, err := ioutil.ReadAll(closer)
	fmt.Println(string(bytes))
}
  1. 創建POST請求
package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	url := "https://www.shirdon.com/comment/add"
	body := `{"userId": 1, "articleId": 1, "comment": 這是一條評論}`
	resp, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(body)))
	if err != nil {
		fmt.Println("err:", err)
	}
	bytes, err := ioutil.ReadAll(resp.Body)
	fmt.Println(string(bytes))
}
  1. 創建PUT請求
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

func main() {
	url := "https://www.shirdon.com/comment/update"
	payload := strings.NewReader(`{"userId": 1, "articleId": 1, "comment": 這是一條評論}`)
	req, _ := http.NewRequest("PUT", url, payload)
	req.Header.Add("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("err:", err)
	}
	defer res.Body.Close()
	bytes, err := ioutil.ReadAll(res.Body)
	fmt.Println(string(res))
	fmt.Println(string(bytes))
}
  1. 創建DELETE請求
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

func main() {
	url := "https://www.shirdon.com/comment/delete"
	payload := strings.NewReader(`{"userId": 1, "articleId": 1, "comment": 這是一條評論}`)
	req, _ := http.NewRequest("DELETE", url, payload)
	req.Header.Add("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("err:", err)
	}
	defer res.Body.Close()
	bytes, err := ioutil.ReadAll(res.Body)
	fmt.Println(string(res))
	fmt.Println(string(bytes))
}
  1. 請求頭設置
type Header map[string][]string
headers := http.Header{"token": {"feeowiwpor23dlspweh"}}
headers.Add("Accept-Charset", "UTF-8")
headers.Set("Host", "www.shirdon.com")
headers.Set("Location", "www.baidu.com")

2.4 html/template包

text/template處理任意格式的文本,html/template生成可對抗代碼注入的安全html文檔。

2.4.1 模板原理

  1. 模板和模板引擎 模板是事先定義好的不變的html文檔,模板渲染使用可變數據替換html文檔中的標記。模板用於顯示和數據分離(前後端分離)。模板技術,本質是模板引擎利用模板文件和數據生成html文檔。
  2. Go語言模板引擎
  • 模板文件後綴名通常為.tmpl和.tpl,UTF-8編碼
  • 模板文件中{{和}}包裹和標識傳入數據
  • 點號(.)訪問數據,{{.FieldName}}訪問字段
  • 除{{和}}包裹內容外,其他內容原樣輸出

使用: (1)定義模板文件 按照相應語法規則去定義。 (2)解析模板文件 創建指定模板名稱的模板對象

func New(name string) *Template

解析模板內容

func (t *Template) Parse(src string) (*Template, error)

解析模板文件

func ParseFiles(filenames...string) (*Template, error)

正則匹配解析文件,template.ParaeGlob(“a*”)

func ParseGlob(pattern string) (*Template, error)

(3)渲染模板文件

func (t *Template) Execute(wr io.Writer, data interface{}) error

//配合ParseFiles()函數使用,需指定模板名稱
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

2.4.2 使用html/template包

  1. 第1個模板 template_example.tmpl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>模板使用示例</title>
</head>
<body>
   <p>加油,小夥伴, {{ . }} </p>
</body>
</html>
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

func helloHandleFunc(w http.ResponseWriter, r *http.Request) {
	// 1. 解析模板
	t, err := template.ParseFiles("./template_example.tmpl")
	if err != nil {
		fmt.Println("template parsefile failed, err:", err)
		return
	}
	// 2.渲染模板
	name := "我愛Go語言"
	t.Execute(w, name)
}

func main() {
	http.HandleFunc("/", helloHandleFunc)
	http.ListenAndServe(":8086", nil)
}
  1. 模板語法 模板語法都包含在{{和}}中間。
type UserInfo struct {
	Name string
	Gender string
	Age int
}

func sayHello(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("./hello.html")
	if err != nil {
		fmp.Println("create template failed, err:", err)
		return
	}

	user := UserInfo {
		Name: "張三",
		Gender: "男",
		Age: 28,
	}
	tmpl.Execute(w, user)
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
   <p>Hello {{.Name}}</p>
   <p>性別:{{.Gender}}</p>
   <p>年齡:{{.Age}}</p>
</body>
</html>

常用語法:

  • 注釋
{{/* 這是一個注釋,不會解析 */}}
  • 管道(pipeline) 產生數據的操作,{{.Name}}等。支持|鏈接多個命令,類似UNIX下管道。
  • 變量 變量捕獲管道的執行結果。
$variable := pipeline
  • 條件判斷
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
  • range關鍵字
{{range pipeline}} T1 {{end}}
{{range pipeline}} T1 {{else}} T0 {{end}}
package main

import (
	"log"
	"os"
	"text/template"
)

func main() {
	//創建一個模版
	rangeTemplate := `
{{if .Kind}}
{{range $i, $v := .MapContent}}
{{$i}} => {{$v}} , {{$.OutsideContent}}
{{end}}
{{else}}
{{range .MapContent}}
{{.}} , {{$.OutsideContent}}
{{end}}    
{{end}}`

	str1 := []string{"第一次 range", "用 index 和 value"}
	str2 := []string{"第二次 range", "沒有用 index 和 value"}

	type Content struct {
		MapContent     []string
		OutsideContent string
		Kind           bool
	}
	var contents = []Content{
		{str1, "第一次外面的內容", true},
		{str2, "第二次外面的內容", false},
	}

	// 創建模板並將字符解析進去
	t := template.Must(template.New("range").Parse(rangeTemplate))

	// 接收並執行模板
	for _, c := range contents {
		err := t.Execute(os.Stdout, c)
		if err != nil {
			log.Println("executing template:", err)
		}
	}
}
/*
//輸出
0 => 第一次 range, 第一次外面的內容
1 => 用 index 和 value, 第一次外面的內容

第二次 range, 第二次外面的內容
沒有用 index 和 value, 第二次外面的內容
*/
  • with關鍵字
{{with pipeline}} T1 {{end}}
{{with pipeline}} T1 {{else}} T0 {{end}}
  • 比較函數 比較函數只適用於基本函數(或重定義的基本類型,如type Banance float32),整數和浮點數不能相互比較。 布爾函數將任何類型的零值視為假。 只有eq可以接受2個以上參數。
{{eq arg1 arg2 arg3}}
eq
ne
lt
le
gt
ge
  • 預定義函數
函數名功能
and返回第1個空參數或最後一個參數,所有參數都執行。and x y等價於if x then y else x
or返回第1個非空參數或最後一個參數,所有參數都執行。and x y等價於if x then y else x
not
len長度
indexindex y 1 2 3, index[1][2][3]
printfmt.Sprint
printffmt.Sprintf
printlnfmt.Sprintln
htmlhtml逸碼等價表示
urlquery可嵌入URL查詢的逸碼等價表示
jsJavaScript逸碼等價表示
callcall func a b, func(a, b);1或2個返回值,第2個為error,非nil會中斷並返回給調用者。
  • 自定義函數 模板對象t的函數字典加入funcMap內的鍵值對。funcMap某個值不是函數類型,或該函數類型不符合要求,會panic。返回*Template便於鏈式調用。
func (t *Template) Funcs(funcMap FuncMap) *Template

FuncMap映射函數要求1或2個返回值,第2個為error,非nil會中斷並返回給調用者。

type FuncMap map[string]interface{}
package main

import (
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
)

func Welcome() string { //沒參數
	return "Welcome"
}

func Doing(name string) string { //有參數
	return name + ", Learning Go Web template "
}

func sayHello(w http.ResponseWriter, r *http.Request) {
	htmlByte, err := ioutil.ReadFile("./funcs.html")
	if err != nil {
		fmt.Println("read html failed, err:", err)
		return
	}
	// 自定義一個匿名模板函數
	loveGo := func() (string) {
		return "歡迎一起學習《Go Web編程實戰派從入門到精通》"
	}
	// 採用鏈式操作在Parse()方法之前調用Funcs添加自定義的loveGo函數
	tmpl1, err := template.New("funcs").Funcs(template.FuncMap{"loveGo": loveGo}).Parse(string(htmlByte))
	if err != nil {
		fmt.Println("create template failed, err:", err)
		return
	}
	funcMap := template.FuncMap{
		//在FuncMap中聲明相應要使用的函數,然後就能夠在template字符串中使用該函數
		"Welcome": Welcome,
		"Doing":   Doing,
	}
	name := "Shirdon"
	tmpl2, err := template.New("test").Funcs(funcMap).Parse("{{Welcome}}<br/>{{Doing .}}")
	if err != nil {
		panic(err)
	}

	// 使用user渲染模板,並將結果寫入w
	tmpl1.Execute(w, name)
	tmpl2.Execute(w, name)
}

func main() {
	http.HandleFunc("/", sayHello)
	http.ListenAndServe(":8087", nil)
}

funcs.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<meta http-equiv="X-UA-Compatible" content="ie=edge">
		<title>tmpl test</title>
	</head>
	<body>
		<h1>{{loveGo}}</h1>
	</body>
</html>
  • 模板嵌套 可以通過文件嵌套和define定義
{{define "name"}} T {{end}}
{{template "name"}}
{{template "name" pipeline}}
{{block "name" pipeline}} T {{end}}
//等價於
{{define "name"}} T {{end}}
{{template "name" pipeline}}

t.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>tmpl test</title>
</head>
<body>
<h1>測試嵌套template語法</h1>
<hr>
{{template "ul.html"}}
<hr>
{{template "ol.html"}}
</body>
</html>
{{define "ol.html"}}
<h1>這是ol.html</h1>
<ol>
    <li>I love Go</li>
    <li>I love java</li>
    <li>I love c</li>
</ol>
{{end}}

ul.html

<ul>
    <li>注釋</li>
    <li>日誌</li>
    <li>測試</li>
</ul>
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

//定義一個UserInfo結構體
type UserInfo struct {
	Name string
	Gender string
	Age int
}

func tmplSample(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("./t.html", "./ul.html")
	if err != nil {
		fmt.Println("create template failed, err:", err)
		return
	}
	user := UserInfo{
		Name:   "張三",
		Gender: "男",
		Age:    28,
	}
	tmpl.Execute(w, user)
	fmt.Println(tmpl)
}

func main() {
	http.HandleFunc("/", tmplSample)
	http.ListenAndServe(":8087", nil)
}