天天看點

go語言http服務入門詳解

作者:幹飯人小羽
go語言http服務入門詳解

當你在浏覽器中輸入URL時,實際上是在發送一個對Web頁面的請求。該請求被發送到伺服器。伺服器的工作是擷取适當的頁面并将其作為響應發送回浏覽器。

在Web的早期,伺服器通常讀取伺服器硬碟上HTML檔案的内容并将該HTML發送回浏覽器。

go語言http服務入門詳解

但今天,伺服器更常見的是與程式通信來完成請求,而不是讀取檔案。這個程式可以用任何你想要的語言編寫,包括Go!

go語言http服務入門詳解

net/http介紹

Go語言标準庫内建提供了net/http包,涵蓋了HTTP用戶端和服務端的具體實作。使用net/http包,我們可以很友善地編寫HTTP用戶端或服務端的程式。

服務端

預設的Server

首先,我們編寫一個最簡單的Web伺服器。編寫這個Web服務隻需要兩步:

  1. 注冊一個處理器函數(注冊到DefaultServeMux);
  2. 設定監聽的TCP位址并啟動服務;

對應到我們的代碼裡就是這樣的:

package main

import (
	"log"
	"net/http"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("Hello World!"))
}
func main() {

	//1.注冊一個給定模式的處理器函數到DefaultServeMux
	http.HandleFunc("/", sayHello)

	//2.設定監聽的TCP位址并啟動服務
	//參數1:TCP位址(IP+Port)
	//參數2:當設定為nil時表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}
           

運作該程式,通過浏覽器通路,可以看到Hello World!顯示在了頁面上

go語言http服務入門詳解

ListenAndServe使用指定的監聽位址和處理器啟動一個HTTP服務端。當處理器參數是nil時,這表示采用包變量DefaultServeMux作為處理器。

Handle和HandleFunc函數可以向DefaultServeMux添加任意多個處理器函數。

http.HandleFunc

使用Go語言中的net/http包來編寫一個簡單的接收HTTP請求的Server端示例,net/http包是對net包的進一步封裝,專門用來處理HTTP協定的資料。具體的代碼如下:

處理器函數的實作原理:

go語言http服務入門詳解

通過源碼可知,這個函數實際上是調用了預設的DefaultServeMux的HandleFunc方法,這也解釋了我們第一步裡所說的預設的實際注冊到DefaultServeMux。

既然說了http.ListenAndServe的第二個參數為nil時采用預設的DefaultServeMux,那麼如果我們不想采用預設的,而是想自己建立一個ServerMux該怎麼辦呢,http給我們提供了方法,NewServeMux建立并傳回一個新的*ServeMux

// NewServeMux配置設定并傳回一個新的ServeMux。
func NewServeMux() *ServeMux { return new(ServeMux) }
           

如果是我們自己建立的ServeMux,我們隻需要簡單的更新一下代碼:

package main

import (
	"log"
	"net/http"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("Hello World!"))
}
func main() {

	//1.建立一個ServeMux,将處理器函數注冊到serverMux
	serveMux := http.NewServeMux()
	serveMux.HandleFunc("/", sayHello)

	//2.設定監聽的TCP位址并啟動服務
	//參數1:TCP位址(IP+Port)
	//參數2:當設定為nil時表示使用DefaultServeMux,如果指定了則表示使用自定義ServeMux
	err := http.ListenAndServe("127.0.0.1:8080", serveMux)
	log.Fatal(err)
}
           

運作修改後的代碼,和采用預設ServeMux一樣正常運作

http.Handle

如果是使用http的handle方法,則handle的第二個參數需要實作handler接口,要想實作這個接口,就得實作這個接口的serveHTTP方法

go語言http服務入門詳解
go語言http服務入門詳解

示例:

package main

import (
	"log"
	"net/http"
)
type MyHandler struct {}

func (m *MyHandler)ServeHTTP(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("Hello World!"))
}
func main() {

	//1.建立一個ServeMux,将處理器函數注冊到serverMux
	http.Handle("/", &MyHandler{})

	//2.設定監聽的TCP位址并啟動服務
	//參數1:TCP位址(IP+Port)
	//參數2:當設定為nil時表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}
           

http.Request

一個Web伺服器最基本的工作就是接收請求,做出響應。http包幫助我們封裝了一個Request結構體,我們通過這個結構體拿到很多使用者的一次HTTP請求的所有資訊。這個Request結構體定義如下:

type Request struct {
    //Method指定HTTP方法(GET、POST、PUT等)
    //Go的用戶端不支援CONNECT方法
	Method string

    //請求URL相關資訊
	URL *url.URL

    //接收到的請求的協定版本。HTTP用戶端代碼始終使用HTTP/1.1或HTTP2
	Proto      string // "HTTP/1.0"

	//請求頭相關資訊
	Header Header

	//請求主體
	Body io.ReadCloser
    
    //用戶端IP位址
	RemoteAddr string
    
    //請求URI
    RequestURI string
    
    .......
    
	//内容上下文
	ctx context.Context
}
           

我這裡列舉的并不是完整的Request結構體定義,隻是大緻的說明一下。完整的定義以及這些字段的中文含義可以檢視Go語言标準庫文檔。

我們通過通過浏覽器可以發現,我們一次HTTP請求會攜帶很多資訊

go語言http服務入門詳解

這些資訊,我們可以通過http.Request來擷取到

package main

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

func HeaderInfo(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	fmt.Printf("Method: %#v\n", r.Method)
	fmt.Printf("URL: %#v\n", r.URL)
	fmt.Printf("Header: %#v\n", r.Header)
	fmt.Printf("Body: %#v\n", r.Body)
	fmt.Printf("RemoteAddr: %#v\n", r.RemoteAddr)
	_, _ = w.Write([]byte("請求成功,請在終端檢視!"))
}
func main() {

	//1.注冊一個給定模式的處理器函數到DefaultServeMux
	http.HandleFunc("/header", HeaderInfo)

	//2.設定監聽的TCP位址并啟動服務
	//參數1:TCP位址(IP+Port)
	//參數2:當設定為nil時表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}
           
go語言http服務入門詳解

自定義Server

要管理服務端的行為,可以建立一個自定義的Server:

import (
	"fmt"
	"net/http"
	"time"
)
type MyHandler struct {}

func (h *MyHandler) ServeHTTP (w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello world!")
}

func main() {
	var handler MyHandler
	var server = http.Server{
		Addr:              ":8080",
		Handler:           &handler,
		ReadTimeout:       2 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	var err = server.ListenAndServe()
	if err != nil {
		fmt.Printf("http server failed, err: %v\n", err)
		return
	}
}
           

HTML模闆

template包(html/template)實作了資料驅動的模闆,以生成可防止代碼注入的HTML輸出。它提供了與text/template包相同的接口,并且隻要實在輸出HTML的場景,就應該使用html/template而不是text/template。

Go語言模闆的使用可以分為三步:定義模闆檔案、解析模闆檔案和渲染模闆檔案。

定義模闆檔案

定義模闆檔案時需要我們按照相關文法規則去編寫,後面會詳細介紹。

解析模闆檔案

上面定義好了模闆檔案之後,可以使用下面額常用方法去解析模闆檔案,得到模闆對象:

func ParseFS(fs fs.FS, patterns ...string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)
func ParseGlob(pattern string) (*Template, error)
           

當然,你也可以使用func New(name string) *Template函數建立一個名為name的模闆,然後對其調用上面的方法去解析模闆字元串或模闆檔案。

模闆渲染

渲染模闆簡單來說就是使用資料去填充模闆,當然實際上可能會複雜很多。

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

基本示例

定義模闆檔案

我們按照Go模闆文法定義一個index.html的模闆檔案,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <h2>{{.}}</h2>
    <img src="./img/pyy.jpeg" style="width: 30%" alt="彭于晏">
</body>
</html>
           

解析和渲染模闆

建立一個main.go檔案,服務端代碼如下

package main

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

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定檔案生成模闆對象
	tmpl, err := template.ParseFiles("./public/index.html")
	if err != nil {
		log.Fatal(err)
	}
	err = tmpl.Execute(w, "彭于晏")
	if err != nil {
		log.Fatal(err)
	}
}

func main() {

	//1.注冊給定模式的處理器函數到DefaultServeMux
	files := http.FileServer(http.Dir("./public"))
	http.Handle("/", files)
	http.HandleFunc("/index", index)

	//2.設定監聽的TCP位址并啟動服務
	//參數1:TCP位址(IP+Port)
	//參數2:當設定為nil時表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}
           

項目結構如下:

.
├── go.mod
├── main.go
├── public
    ├── img
    │   └── pyy.jpeg
    └── index.html
           

将上面的main.go檔案編譯執行,然後使用浏覽器通路http://127.0.0.1:8080就能看到頁面上顯示了“彭于晏”。 這就是一個最簡單的模闆渲染的示例,Go語言模闆引擎詳細用法請往下閱讀。

{{.}}

模闆文法都包含在{{和}}中間,其中{{.}}中的點表示目前對象。

當我們傳入一個結構體對象時,我們可以根據.來通路結構體的對應字段。例如:

package main

import (
	"html/template"
	"log"
	"net/http"
)
type Address struct {
	Province string
	City     string
}
type User struct {
	Name string
	Age  int
	Addr Address
}

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定檔案生成模闆對象
	tmpl, err := template.ParseFiles("./public/index.html")
	if err != nil {
		log.Fatal(err)
	}
	user := User{
		Name: "彭于晏",
		Age:  28,
		Addr: Address{Province: "台灣省", City: "澎湖縣"},
	}
	//利用給定資料渲染模闆,并将結果寫入
	err = tmpl.Execute(w, user)
	if err != nil {
		log.Fatal(err)
	}
}

func main() {

	//1.注冊給定模式的處理器函數到DefaultServeMux
	files := http.FileServer(http.Dir("./public"))
	http.Handle("/", files)
	http.HandleFunc("/index", index)

	//2.設定監聽的TCP位址并啟動服務
	//參數1:TCP位址(IP+Port)
	//參數2:當設定為nil時表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}
           

模闆檔案index.html内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <p>姓名:{{.Name}}</p>
    <p>年齡:{{.Age}}</p>
    <p>位址:{{.Addr.Province}}-{{.Addr.City}}</p>
    <p>你好呀{{/* 這是注釋 */}},彭于晏</p>
    <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
</body>
</html>
           

顯示效果如下圖所示

go語言http服務入門詳解

同理,當我們傳入的變量是map時,也可以在模闆檔案中通過.根據key來取值。

注意:在模闆中使用的變量應當是可導出的,否則的話對應的變量無法顯示。

注釋

//注釋,執行時會忽略。可以多行。注釋不能嵌套,并且必須緊貼分界符始止。
{{/* 這是注釋 */}}
           

pipeline

pipeline是指産生資料的操作。比如{{.}}、{{.Name}}等。Go的模闆文法中支援使用管道符号|連結多個指令,用法和unix下的管道類似:|前面的指令會将運算結果(或傳回值)傳遞給後一個指令的最後一個位置。

注意:并不是隻有使用了|才是pipeline。Go的模闆文法中,pipeline的概念是傳遞資料,隻要能産生資料的,都是pipeline

示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <div align="center">
        <p>姓名:{{.Name|len}}</p>
        <p>年齡:{{.Age}}</p>
        <p>位址:{{.Addr.Province}}-{{.Addr.City}}</p>
        <p>你好呀{{/* 這是注釋 */}},彭于晏</p>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
           

變量

我們還可以在模闆中聲明變量,用來儲存傳入模闆的資料或其他語句生成的結果。具體文法如下:

$obj := {{.}}
           

其中$obj是變量的名字,在後續的代碼中就可以使用該變量了。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <div align="center">
        {{$obj := .Name}}
        <p>姓名:{{ $obj }}</p>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
           

移除空格

template引擎在進行替換的時候,是完全按照文本格式進行替換的。除了需要評估和替換的地方,所有的行分隔符、空格等等空白都原樣保留。是以,對于要解析的内容,不要随意縮進、随意換行。

有時候我們在使用模闆文法的時候會不可避免的引入一下空格或者換行符,這樣模闆最終渲染出來的内容可能就和我們想的不一樣,這個時候可以使用{{-文法去除模闆内容左側的所有空白符号, 使用-}}去除模闆内容右側的所有空白符号。

注意:-要緊挨{{和}},同時與模闆值之間需要使用空格分隔。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <div align="center">
        <p>姓名:{{- .Name -}} -king</p> <!-- 彭于晏-king -->
        <p>姓名:{{ .Name }} -king</p> <!-- 彭于晏 -king -->
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
           

if語句

Go模闆文法中的條件判斷有以下幾種:

{{if pipeline}} T1 {{end}}

{{if pipeline}} T1 {{else}} T0 {{end}}

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
           

需要注意的是,pipeline為false的情況是各種資料對象的0值:數值0,指針或接口是nil,數組、slice、map或string則是len為0。

示例:

<!-- 由于.Name不是空字元串,是以可以渲染出彭于晏 -->
<p>姓名:{{if .Name}} 彭于晏 {{end}}</p>
           

range

Go的模闆文法中使用range關鍵字進行周遊,有以下兩種寫法,其中pipeline的值必須是數組、切片、map或者channel。

{{range pipeline}} T1 {{end}}
如果pipeline的值其長度為0,不會有任何輸出

{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其長度為0,則會執行T0。
           

需注意的是,range的參數部分是pipeline,是以在疊代的過程中是可以進行指派的。但有兩種指派情況:

{{range $value := .}}
{{range $key,$value := .}}
           

如果range中隻指派給一個變量,則這個變量是目前正在疊代元素的值。如果指派給兩個變量,則第一個變量是索引值(數組/切片是數值,map是key),第二個變量是目前正在疊代元素的值。

示例:

index.html檔案

{{range $x := .}}
<p>{{$x}}</p>
{{else}}
<p>這是一個空切片</p>
{{end}}
           

也可以這樣

{{range .}}
<p>{{.}}</p>
{{else}}
<p>這是一個空切片</p>
{{end}}
           

main.go檔案

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定檔案生成模闆對象
	tmpl, err := template.ParseFiles("./public/index.html")
	if err != nil {
		log.Fatal(err)
	}
	var arr []int
	//利用給定資料渲染模闆,并将結果寫入
	err = tmpl.Execute(w, arr)
	if err != nil {
		log.Fatal(err)
	}
}
           

with

{{with pipeline}} T1 {{end}}
如果pipeline為empty不産生輸出,否則将dot設為pipeline的值并執行T1。不修改外面的dot。

{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline為empty,不改變dot并執行T0,否則dot設為pipeline的值并執行T1。
           

對于第一種格式,當pipeline不為0值的時候,點"."設定為pipeline運算的值,否則跳過。對于第二種格式,當pipeline為0值時,執行else語句塊,否則"."設定為pipeline運算的值,并執行T1。

示例:

<p>{{with "彭于晏"}} {{.}} {{end}}</p>
           

上面将輸出彭于晏,因為"."已經設定為"彭于晏"。

比較函數

布爾函數會将任何類型的零值視為假,其餘視為真。

下面是定義為函數的二進制比較運算的集合:

eq      如果arg1 == arg2則傳回真
ne      如果arg1 != arg2則傳回真
lt      如果arg1 < arg2則傳回真
le      如果arg1 <= arg2則傳回真
gt      如果arg1 > arg2則傳回真
ge      如果arg1 >= arg2則傳回真
           

為了簡化多參數相等檢測,eq(隻有eq)可以接受2個或更多個參數,它會将第一個參數和其餘參數依次比較,傳回下式的結果:

{{eq arg1 arg2 arg3}}
           

等價于

arg1==arg2 || arg1==arg3
           

比較函數隻适用于基本類型(或重定義的基本類型,如”type Celsius float32”)。但是,整數和浮點數不能互相比較。

示例:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <div align="center">
        {{if (eq .Name "彭于晏") }}
        <p>彭于晏</p>
        {{else}}
        <p>晏晏</p>
        {{end}}
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
           

預定義函數

預定義的全局函數如下:

and
    函數傳回它的第一個empty參數或者最後一個參數;
    就是說"and x y"等價于"if x then y else x";所有參數都會執行;
or
    傳回第一個非empty參數或者最後一個參數;
    亦即"or x y"等價于"if x then x else y";所有參數都會執行;
not
    傳回它的單個參數的布爾值的否定
len
    傳回它的參數的整數類型長度
index
    執行結果為第一個參數以剩下的參數為索引/鍵指向的值;
    如"index x 1 2 3"傳回x[1][2][3]的值;每個被索引的主體必須是數組、切片或者字典。
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    傳回與其參數的文本表示形式等效的轉義HTML。
    這個函數在html/template中不可用。
urlquery
    以适合嵌入到網址查詢中的形式傳回其參數的文本表示的轉義值。
    這個函數在html/template中不可用。
js
    傳回與其參數的文本表示形式等效的轉義JavaScript。
call
    執行結果是調用第一個參數的傳回值,該參數必須是函數類型,其餘參數作為調用該函數的參數;
    如"call .X.Y 1 2"等價于go語言裡的dot.X.Y(1, 2);
    其中Y是函數類型的字段或者字典的值,或者其他類似情況;
    call的第一個參數的執行結果必須是函數類型的值(和預定義函數如print明顯不同);
    該函數類型值必須有1到2個傳回值,如果有2個則後一個必須是error接口類型;
    如果有2個傳回值的方法傳回的error非nil,模闆執行會中斷并傳回給調用模闆執行者該錯誤;
           

示例:

<div align="center">
    <!-- if (.Name == '彭于晏' && .Age == 28)  -->
    {{if (and (eq .Name "彭于晏") (eq .Age 28)) }}
    <p>彭于晏</p>
    {{else}}
    <p>晏晏</p>
    {{end}}
</div>
           

自定義函數

Go的模闆支援自定義函數。需要注意的是自定義函數必須在解析模闆之前。

main.go檔案

func index(w http.ResponseWriter, r *http.Request) {
	fn := func(name string) string {
		return name + ",你好!"
	}
	//自定義函數必須在解析模闆之前
	tmpl := template.New("index.html").Funcs(template.FuncMap{"hello": fn})
	//解析指定檔案生成模闆對象
	tmpl, err := tmpl.ParseFiles("./public/index.html")
	if err != nil {
		log.Fatal(err)
	}
	var user = User{Name: "彭于晏", Age: 28}
	//利用給定資料渲染模闆,并将結果寫入
	err = tmpl.Execute(w, user)
	if err != nil {
		log.Fatal(err)
	}
}
           

index.html檔案

<p>{{hello .Name}}</p>
           

嵌套template

我們可以在template中嵌套其他的template。這個template可以是單獨的檔案,也可以是通過define定義的template。

舉個例子: index.html檔案内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <div align="center">
        <p>嵌套template示例</p>
        <br>
        {{template "ul.html"}}
        <br>
        {{template "ol.tmpl"}}
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
{{define "ol.tmpl"}}
<ol>
    <li>彭于晏</li>
    <li>高圓圓</li>
    <li>趙又廷</li>
</ol>
{{end}}
           

ul.html檔案内容如下:

<ul>
  <li>彭于晏</li>
  <li>高圓圓</li>
  <li>趙又廷</li>
</ul>
           

main.go檔案

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定檔案生成模闆對象
	tmpl, err := template.ParseFiles("./public/index.html", "./public/ul.html")
	if err != nil {
		log.Fatal(err)
	}

	var user = User{Name: "彭于晏", Age: 28}
	//利用給定資料渲染模闆,并将結果寫入
	err = tmpl.Execute(w, user)
	if err != nil {
		log.Fatal(err)
	}
}
           

注意:在解析模闆時,被嵌套的模闆一定要在後面解析,例如上面的示例中index.html模闆中嵌套了ul.html,是以ul.html要在index.html後進行解析。

block

根據官方文檔的解釋:block等價于define定義一個名為name的模闆,并在"有需要"的地方執行這個模闆,執行時将"."設定為pipeline的值。

{{block "name" pipeline}} T1 {{end}}
           

但應該注意,block的第一個動作是執行名為name的模闆,如果不存在,則在此處自動定義這個模闆,并執行這個臨時定義的模闆。換句話說,block可以認為是設定一個預設模闆。

例如:

{{block "T1" .}} one {{end}}
           

它首先表示{{template "T1" .}},也就是說先找到T1模闆,如果T1存在,則執行找到的T1,如果沒找到T1,則臨時定義一個{{define "T1"}} one {{end}},并執行它。

示例:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <div align="center">
        <p>block用法示例</p>
        {{block "content" .}}
            <p style="color: blue;">彭于晏</p>
        {{end}}
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
           

red.html

{{define "content"}}
  <p style="color: red;">彭于晏</p>
{{end}}
           

main.go

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定檔案生成模闆對象
	var tmpl *template.Template
	var err error
	if rand.Intn(2) > 0 {
		tmpl, err = template.ParseFiles("./public/index.html", "./public/red.html")
	} else {
		tmpl, err = template.ParseFiles("./public/index.html")
	}
	if err != nil {
		log.Fatal(err)
	}

	//利用給定資料渲染模闆,并将結果寫入
	err = tmpl.Execute(w, nil)
	if err != nil {
		log.Fatal(err)
	}
}
           

這樣示例的效果就是重新整理頁面,彭于晏的名字顔色在紅藍之間反複變化

go語言http服務入門詳解

辨別符

Go标準庫的模闆引擎使用的花括号{{和}}作為辨別,而許多前端架構(如Vue和 AngularJS)也使用{{和}}作為辨別符,是以當我們同時使用Go語言模闆引擎和以上前端架構時就會出現沖突,這個時候我們需要修改辨別符,修改前端的或者修改Go語言的。這裡示範如何修改Go語言模闆引擎預設的辨別符:

template.New("index.html").Delims("{[", "]}").ParseFiles("./public/index.html")
           

上下文感覺

對于html/template包,有一個很好用的功能:上下文感覺。text/template沒有該功能。

上下文感覺具體指的是根據所處環境css、js、html、url的path、url的query,自動進行不同格式的轉義。

例如,一個handler函數的代碼如下:

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定檔案生成模闆對象
	tmpl, err := template.ParseFiles("./public/index.html")

	if err != nil {
		log.Fatal(err)
	}

	str := `I asked: <i>"What's up?"</i>`
	//利用給定資料渲染模闆,并将結果寫入
	err = tmpl.Execute(w, str)
	if err != nil {
		log.Fatal(err)
	}
}
           

上面str是Execute的第二個參數,它的内容是包含了特殊符号的字元串。

下面是index.html檔案的内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <div align="center">
        <p>html/template上下文感覺示例</p>
        <div>{{ . }}</div>
        <div><a href="/{{ . }}">Path</a></div>
        <div><a href="/?q={{ . }}">Query</a></div>
        <div><a onclick="f('{{ . }}')">Onclick</a></div>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
           

上面index.html中有4個不同的環境,分别是html環境、url的path環境、url的query環境以及js環境。雖然對象都是{{.}},但解析執行後的值是不一樣的。如果使用curl擷取源代碼,結果将如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <div align="center">
        <p>html/template上下文感覺示例</p>
        <div>I asked: <i>"What's up?"</i></div>
        <div><a href="/I%20asked:%20%3ci%3e%22What%27s%20up?%22%3c/i%3e">Path</a></div>
        <div><a href="/?q=I%20asked%3a%20%3ci%3e%22What%27s%20up%3f%22%3c%2fi%3e">Query</a></div>
        <div><a onclick="f('I asked: \u003ci\u003e\u0022What\u0027s up?\u0022\u003c\/i\u003e')">Onclick</a></div>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
           

html/template針對的是需要傳回HTML内容的場景,在模闆渲染過程中會對一些有風險的内容進行轉義,以此來防範跨站腳本攻擊。

例如,定義下面的模闆檔案:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index頁面</title>
</head>
<body>
    <div align="center">
        <p>html/template上下文感覺示例</p>
        {{ . }}
        <br>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
           

handler控制器代碼如下:

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定檔案生成模闆對象
	tmpl, err := template.ParseFiles("./public/index.html")

	if err != nil {
		log.Fatal(err)
	}
	str := `<script>alert('嘿嘿嘿')</script>`
	//利用給定資料渲染模闆,并将結果寫入
	err = tmpl.Execute(w, str)
	if err != nil {
		log.Fatal(err)
	}
}
           

運作代碼顯示如下

go語言http服務入門詳解

如果我們将包由html/template改為text/template,重新運作代碼,可以發現被注入了js代碼

go語言http服務入門詳解

用戶端

http包提供了很多通路Web伺服器的函數,比如http.Get()、http.Post()、http.Head()等,讀到的響應封包資料被儲存在 Response 結構體中。

我們通過檢視http.Get()方法的源碼可知,實際上是預設的DefaultClient調用自己的Get方法。

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

真正用來發送請求的Client結構體定義如下:

type Client struct {
	// Transport指定執行獨立、單次HTTP請求的機制。
	// 如果Transport為nil,則使用DefaultTransport。
	Transport RoundTripper

	// CheckRedirect指定處理重定向的政策。
	// 如果CheckRedirect不為nil,用戶端會在執行重定向之前調用本函數字段。
	// 參數req和via是将要執行的請求和已經執行的請求(切片,越新的請求越靠後)。
	// 如果CheckRedirect傳回一個錯誤,本類型的Get方法不會發送請求req,
	// 而是傳回之前得到的最後一個回複和該錯誤。(包裝進url.Error類型裡)
	//
	// 如果CheckRedirect為nil,會采用預設政策:連續10此請求後停止。
	CheckRedirect func(req *Request, via []*Request) error

	// Jar指定cookie管理器。
    // 如果Jar為nil,請求中不會發送cookie,響應的cookie會被忽略。
	Jar CookieJar

	// Timeout指定本類型的值執行請求的時間限制。
    // 該逾時限制包括連接配接時間、重定向和讀取回複主體的時間。
    // 計時器會在Head、Get、Post或Do方法傳回後繼續運作并在逾時後中斷回複主體的讀取。
    //
    // Timeout為零值表示不設定逾時。
    //
    // Client執行個體的Transport字段必須支援CancelRequest方法,
    // 否則Client會在試圖用Head、Get、Post或Do方法執行請求時傳回錯誤。
    // 本類型的Transport字段預設值(DefaultTransport)支援CancelRequest方法。
	Timeout time.Duration
}
           

基本的HTTP/HTTPS請求

Get、Head、Post和PostForm函數發出HTTP/HTTPS請求。

resp, err := http.Get("http://example.com/")
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
resp, err := http.PostForm("http://example.com/form", url.Values{"key": {"Value"}, "id": {"123"}})
           

程式在使用完response後必須關閉回複的主體。

resp, err := http.Get("http://example.com/")
if err != nil {
	// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
           

GET請求示例

使用net/http包編寫一個簡單的發送HTTP請求的Client端,代碼如下:

package main

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

func main() {
	resp, err := http.Get("http://127.0.0.1:8080")
	if err != nil {
		log.Fatalf("http.Get()函數執行錯誤,錯誤為:%v\n", err)
	}
	defer resp.Body.Close()

	//由于ioutil包的歧義性,1.16版本之後不再建議使用ioutil包,包内方法的具體實作都改為了調用的其它包方法
	//ioutil.ReadAll(resp.Body)
	body, err := io.ReadAll(resp.Body)

	if err != nil {
		log.Fatalf("io.RedAll()函數執行出錯,錯誤為:%v\n", err)
	}
	fmt.Println(string(body))
}
           

服務端代碼如下:

package main

import (
	"log"
	"net/http"
)

func index(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}
func main() {

	http.HandleFunc("/", index)
	err := http.ListenAndServe(":8080", nil)
	log.Fatal(err)
}
           

将上面的代碼儲存之後編譯成可執行檔案,執行之後就能在終端列印hello world網站首頁的内容了,我們的浏覽器其實就是一個發送和接收HTTP協定資料的用戶端,我們平時通過浏覽器通路網頁其實就是從網站的伺服器接收HTTP資料,然後浏覽器會按照HTML、CSS等規則将網頁渲染展示出來。

帶參數的GET請求示例

關于GET請求的參數需要使用Go語言内置的net/url這個标準庫來處理。

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
)

func main() {
	//1.處理請求參數
	params := url.Values{}
	params.Set("name", "itbsl")
	params.Set("hobby", "fishing")

	//2.設定請求URL
	rawUrl := "http://127.0.0.1:8080"
	reqUrl, err := url.ParseRequestURI(rawUrl)
	if err != nil {
		fmt.Printf("url.ParseRequestURI()函數執行錯誤,錯誤為:%v\n", err)
		return
	}

	//3.整合請求URL和參數
	//Encode方法将請求參數編碼為url編碼格式("bar=baz&foo=foz"),編碼時會以鍵進行排序。
	reqUrl.RawQuery = params.Encode()

	//4.發送HTTP請求
	//說明:reqURL.String() String将URL重構為一個合法URL字元串。
	resp, err := http.Get(reqUrl.String())
	if err != nil {
		log.Fatalf("http.Get()函數執行錯誤,錯誤為:%v\n", err)
	}
	defer resp.Body.Close()
	
	//5.一次性讀取響應的所有内容
	body, err := io.ReadAll(resp.Body)

	if err != nil {
		log.Fatalf("io.RedAll()函數執行出錯,錯誤為:%v\n", err)
	}
	fmt.Println(string(body))
}
           

對應的Server端代碼如下:

package main

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

func index(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	params := r.URL.Query()
	fmt.Printf("name: %v, hobby: %v\n", params.Get("name"), params.Get("nobody"))
	w.Write([]byte("hello world"))
}
func main() {

	http.HandleFunc("/", index)
	err := http.ListenAndServe(":8080", nil)
	log.Fatal(err)
}
           

POST請求示例

面示範了使用net/http包發送GET請求的示例,發送POST請求的示例代碼如下:

package main

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

func main() {
	url := "http://127.0.0.1:8080"
	// 表單資料
	//contentType := "application/x-www-form-urlencoded"
	//data := "name=itbsl&age=18"
	// json
	contentType := "application/json"
	data := `{"name":"itbsl","age":18}`
	resp, err := http.Post(url, contentType, strings.NewReader(data))
	if err != nil {
		fmt.Printf("post failed, err:%v\n", err)
		return
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("get resp failed,err:%v\n", err)
		return
	}
	fmt.Println(string(body))
}
           

對應的Server端HandlerFunc如下:

func index(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	// 1. 請求類型是application/x-www-form-urlencoded時解析form資料
	//r.ParseForm()
	//fmt.Println(r.PostForm) // 列印form資料
	//fmt.Println(r.PostForm.Get("name"), r.PostForm.Get("age"))
	// 2. 請求類型是application/json時從r.Body讀取資料
	body, err := io.ReadAll(r.Body)
	if err != nil {
		fmt.Printf("read request.Body failed, err:%v\n", err)
		return
	}
	fmt.Println(string(body))
	answer := `{"status": "ok"}`
	w.Write([]byte(answer))
}
           

自定義Client

要管理HTTP用戶端的頭域、重定向政策和其他設定,建立一個Client:

package main

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

func main() {
	client := &http.Client{}
	req, err := http.NewRequest("GET", "http://127.0.0.1:8080", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("sign", "sign_aes")
	resp, err := client.Do(req)
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(body))
}
           

繼續閱讀