Go的研習筆記-day13(以Java的視角學習Go)

原文鏈接:https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/16.10.md

網絡,模板和網頁應用
Go 在編寫 web 應用方面非常得力,框架也有不太成熟的go ui等

  • tcp 服務器
    編寫一個簡單的客戶端-服務器應用,一個(web)服務器應用需要響應衆多客戶端的併發請求:Go 會爲每一個客戶端產生一個協程用來處理請求。我們需要使用 net 包中網絡通信的功能。它包含了處理 TCP/IP 以及 UDP 協議、域名解析等方法。
package main

import (
	"fmt"
	"net"
)

func main() {
	fmt.Println("Starting the server ...")
	// 創建 listener
	listener, err := net.Listen("tcp", "localhost:50000")
	if err != nil {
		fmt.Println("Error listening", err.Error())
		return //終止程序
	}
	// 監聽並接受來自客戶端的連接
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Error accepting", err.Error())
			return // 終止程序
		}
		go doServerStuff(conn)
	}
}

func doServerStuff(conn net.Conn) {
	for {
		buf := make([]byte, 512)
		len, err := conn.Read(buf)
		if err != nil {
			fmt.Println("Error reading", err.Error())
			return //終止程序
		}
		fmt.Printf("Received data: %v", string(buf[:len]))
	}
}

在 main() 中創建了一個 net.Listener 類型的變量 listener,他實現了服務器的基本功能:用來監聽和接收來自客戶端的請求(在 localhost 即 IP 地址爲 127.0.0.1 端口爲 50000 基於TCP協議)。Listen() 函數可以返回一個 error 類型的錯誤變量。用一個無限 for 循環的 listener.Accept() 來等待客戶端的請求。客戶端的請求將產生一個 net.Conn 類型的連接變量。然後一個獨立的協程使用這個連接執行 doServerStuff(),開始使用一個 512 字節的緩衝 data 來讀取客戶端發送來的數據,並且把它們打印到服務器的終端,len 獲取客戶端發送的數據字節數;當客戶端發送的所有數據都被讀取完成時,協程就結束了。這段程序會爲每一個客戶端連接創建一個獨立的協程。必須先運行服務器代碼,再運行客戶端代碼。

客戶端代碼寫在另一個文件 client.go 中

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	//打開連接:
	conn, err := net.Dial("tcp", "localhost:50000")
	if err != nil {
		//由於目標計算機積極拒絕而無法創建連接
		fmt.Println("Error dialing", err.Error())
		return // 終止程序
	}

	inputReader := bufio.NewReader(os.Stdin)
	fmt.Println("First, what is your name?")
	clientName, _ := inputReader.ReadString('\n')
	// fmt.Printf("CLIENTNAME %s", clientName)
	trimmedClient := strings.Trim(clientName, "\r\n") // Windows 平臺下用 "\r\n",Linux平臺下使用 "\n"
	// 給服務器發送信息直到程序退出:
	for {
		fmt.Println("What to send to the server? Type Q to quit.")
		input, _ := inputReader.ReadString('\n')
		trimmedInput := strings.Trim(input, "\r\n")
		// fmt.Printf("input:--%s--", input)
		// fmt.Printf("trimmedInput:--%s--", trimmedInput)
		if trimmedInput == "Q" {
			return
		}
		_, err = conn.Write([]byte(trimmedClient + " says: " + trimmedInput))
	}
}

客戶端通過 net.Dial 創建了一個和服務器之間的連接。
它通過無限循環從 os.Stdin 接收來自鍵盤的輸入,直到輸入了“Q”。注意裁剪 \r 和 \n 字符(僅 Windows 平臺需要)。裁剪後的輸入被 connection 的 Write 方法發送到服務器。
當然,服務器必須先啓動好,如果服務器並未開始監聽,客戶端是無法成功連接的。
如果在服務器沒有開始監聽的情況下運行客戶端程序,客戶端會停止並打印出以下錯誤信息:對tcp 127.0.0.1:50000發起連接時產生錯誤:由於目標計算機的積極拒絕而無法創建連接。
打開命令提示符並轉到服務器和客戶端可執行程序所在的目錄,Windows 系統下輸入server.exe(或者只輸入server),Linux系統下輸入./server。
接下來控制檯出現以下信息:Starting the server …
在 Windows 系統中,我們可以通過 CTRL/C 停止程序。
然後開啓 2 個或者 3 個獨立的控制檯窗口,分別輸入 client 回車啓動客戶端程序
以下是服務器的輸出:
Starting the Server …
Received data: IVO says: Hi Server, what’s up ?
Received data: CHRIS says: Are you busy server ?
Received data: MARC says: Don’t forget our appointment tomorrow !
當客戶端輸入 Q 並結束程序時,服務器會輸出以下信息:
Error reading WSARecv tcp 127.0.0.1:50000: The specified network name is no longer available.

在網絡編程中 net.Dial 函數是非常重要的,一旦你連接到遠程系統,函數就會返回一個 Conn 類型的接口,我們可以用它發送和接收數據。Dial 函數簡潔地抽象了網絡層和傳輸層。所以不管是 IPv4 還是 IPv6,TCP 或者 UDP 都可以使用這個公用接口。
以下示例先使用 TCP 協議連接遠程 80 端口,然後使用 UDP 協議連接,最後使用 TCP 協議連接 IPv6 地址:

// make a connection with www.example.org:
package main

import (
	"fmt"
	"net"
	"os"
)

func main() {
	conn, err := net.Dial("tcp", "192.0.32.10:80") // tcp ipv4
	checkConnection(conn, err)
	conn, err = net.Dial("udp", "192.0.32.10:80") // udp
	checkConnection(conn, err)
	conn, err = net.Dial("tcp", "[2620:0:2d0:200::10]:80") // tcp ipv6
	checkConnection(conn, err)
}
func checkConnection(conn net.Conn, err error) {
	if err != nil {
		fmt.Printf("error %v connecting!", err)
		os.Exit(1)
	}
	fmt.Printf("Connection is made with %v\n", conn)
}

下邊也是一個使用 net 包從 socket 中打開,寫入,讀取數據的例子:

package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
	var (
		host          = "www.apache.org"
		port          = "80"
		remote        = host + ":" + port
		msg    string = "GET / \n"
		data          = make([]uint8, 4096)
		read          = true
		count         = 0
	)
	// 創建一個socket
	con, err := net.Dial("tcp", remote)
	// 發送我們的消息,一個http GET請求
	io.WriteString(con, msg)
	// 讀取服務器的響應
	for read {
		count, err = con.Read(data)
		read = (err == nil)
		fmt.Printf(string(data[0:count]))
	}
	con.Close()
}

一個簡單的網頁服務器
http 是比 tcp 更高層的協議,它描述了網頁服務器如何與客戶端瀏覽器進行通信。Go 提供了 net/http 包,我們馬上就來看下。先從一些簡單的示例開始,首先編寫一個“Hello world!”網頁服務器:
我們引入了 http 包並啓動了網頁服務器,和net.Listen(“tcp”, “localhost:50000”) 函數的 tcp 服務器是類似的,使用 http.ListenAndServe(“localhost:8080”, nil) 函數,如果成功會返回空,否則會返回一個錯誤(地址 localhost 部分可以省略,8080 是指定的端口號)。
http.URL 用於表示網頁地址,其中字符串屬性 Path 用於保存 url 的路徑;http.Request 描述了客戶端請求,內含一個 URL 字段。
如果 req 是來自 html 表單的 POST 類型請求,“var1” 是該表單中一個輸入域的名稱,那麼用戶輸入的值就可以通過 Go 代碼 req.FormValue(“var1”) 獲取到。還有一種方法是先執行 request.ParseForm(),然後再獲取 request.Form[“var1”] 的第一個返回參數,就像這樣:
var1, found := request.Form[“var1”]
第二個參數 found 爲 true。如果 var1 並未出現在表單中,found 就是 false。
表單屬性實際上是 map[string][]string 類型。網頁服務器發送一個 http.Response 響應,它是通過 http.ResponseWriter 對象輸出的,後者組裝了 HTTP 服務器響應,通過對其寫入內容,我們就將數據發送給了 HTTP 客戶端。
現在我們仍然要編寫程序,以實現服務器必須做的事,即如何處理請求。這是通過 http.HandleFunc 函數完成的。在這個例子中,當根路徑“/”(url地址是 http://localhost:8080)被請求的時候(或者這個服務器上的其他任意地址),HelloServer 函數就被執行了。這個函數是 http.HandlerFunc 類型的,它們通常被命名爲 Prefhandler,和某個路徑前綴 Pref 匹配。
http.HandleFunc 註冊了一個處理函數(這裏是 HelloServer)來處理對應 / 的請求。
/ 可以被替換爲其他更特定的 url,比如 /create,/edit 等等;你可以爲每一個特定的 url 定義一個單獨的處理函數。這個函數需要兩個參數:第一個是 ReponseWriter 類型的 w;第二個是請求 req。程序向 w 寫入了 Hello 和 r.URL.Path[1:] 組成的字符串:末尾的 [1:] 表示“創建一個從索引爲 1 的字符到結尾的子切片”,用來丟棄路徑開頭的“/”,fmt.Fprintf() 函數完成了本次寫入;另一種可行的寫法是 io.WriteString(w, “hello, world!\n”)。
總結:第一個參數是請求的路徑,第二個參數是當路徑被請求時,需要調用的處理函數的引用。

package main

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

func HelloServer(w http.ResponseWriter, req *http.Request) {
	fmt.Println("Inside HelloServer handler")
	fmt.Fprintf(w, "Hello,"+req.URL.Path[1:])
}

func main() {
	http.HandleFunc("/", HelloServer)
	err := http.ListenAndServe("localhost:8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err.Error())
	}
}
使用命令行啓動程序,會打開一個命令窗口顯示如下文字:

Starting Process E:/Go/GoBoek/code_examples/chapter_14/hello_world_webserver.exe...
然後打開瀏覽器並輸入 url 地址:http://localhost:8080/world,瀏覽器就會出現文字:Hello, world,網頁服務器會響應你在 :8080/ 後邊輸入的內容。

fmt.Println 在服務器端控制檯打印狀態;在每個處理函數被調用時,把請求記錄下來也許更爲有用。
注: 1)前兩行(沒有錯誤處理代碼)可以替換成以下寫法:

http.ListenAndServe(":8080", http.HandlerFunc(HelloServer))
2)fmt.Fprint 和 fmt.Fprintf 都是可以用來寫入 http.ResponseWriter 的函數(他們實現了 io.Writer)。 比如我們可以使用

fmt.Fprintf(w, "<h1>%s<h1><div>%s</div>", title, body)
來構建一個非常簡單的網頁並插入 title 和 body 的值。

如果你需要更多複雜的替換,使用模板包(見 15.7節)

3)如果你需要使用安全的 https 連接,使用 http.ListenAndServeTLS() 代替 http.ListenAndServe()

4)除了 http.HandleFunc("/", Hfunc),其中的 HFunc 是一個處理函數,簽名爲:

func HFunc(w http.ResponseWriter, req *http.Request) {
	...
}
也可以使用這種方式:http.Handle("/", http.HandlerFunc(HFunc))

HandlerFunc 只是定義了上述 HFunc 簽名的別名:

type HandlerFunc func(ResponseWriter, *Request)
它是一個可以把普通的函數當做 HTTP 處理器(Handler)的適配器。如果函數 f 聲明的合適,HandlerFunc(f) 就是一個執行 f 函數的 Handler 對象。

http.Handle 的第二個參數也可以是 T 類型的對象 obj:http.Handle("/", obj)。

如果 T 有 ServeHTTP 方法,那就實現了http 的 Handler 接口:

func (obj *Typ) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	...
}
這個用法也在 Counter 和 Chan 類型上使用。只要實現了 http.Handler,http 包就可以處理任何 HTTP 請求。
  • 訪問並讀取頁面
    在下邊這個程序中,數組中的 url 都將被訪問:會發送一個簡單的 http.Head() 請求查看返回值;它的聲明如下:func Head(url string) (r *Response, err error)
    返回的響應 Response 其狀態碼會被打印出來。
package main

import (
	"fmt"
	"net/http"
)

var urls = []string{
	"http://www.google.com/",
	"http://golang.org/",
	"http://blog.golang.org/",
}

func main() {
	// Execute an HTTP HEAD request for all url's
	// and returns the HTTP status string or an error string.
	for _, url := range urls {
		resp, err := http.Head(url)
		if err != nil {
			fmt.Println("Error:", url, err)
		}
		fmt.Println(url, ": ", resp.Status)
	}
}
  • 寫一個簡單的網頁應用
    下邊的程序在端口 8088 上啓動了一個網頁服務器;SimpleServer 會處理 url /test1 使它在瀏覽器輸出 hello world。FormServer 會處理 url /test2:如果 url 最初由瀏覽器請求,那麼它是一個 GET 請求,返回一個 form 常量,包含了簡單的 input 表單,這個表單裏有一個文本框和一個提交按鈕。當在文本框輸入一些東西並點擊提交按鈕的時候,會發起一個 POST 請求。FormServer 中的代碼用到了 switch 來區分兩種情況。請求爲 POST 類型時,name 屬性 爲 inp 的文本框的內容可以這樣獲取:request.FormValue(“inp”)。然後將其寫回瀏覽器頁面中。在控制檯啓動程序,然後到瀏覽器中打開 url http://localhost:8088/test2 來測試這個程序
package main

import (
	"io"
	"net/http"
)

const form = `
	<html><body>
		<form action="#" method="post" name="bar">
			<input type="text" name="in" />
			<input type="submit" value="submit"/>
		</form>
	</body></html>
`

/* handle a simple get request */
func SimpleServer(w http.ResponseWriter, request *http.Request) {
	io.WriteString(w, "<h1>hello, world</h1>")
}

func FormServer(w http.ResponseWriter, request *http.Request) {
	w.Header().Set("Content-Type", "text/html")
	switch request.Method {
	case "GET":
		/* display the form to the user */
		io.WriteString(w, form)
	case "POST":
		/* handle the form data, note that ParseForm must
		   be called before we can extract form data */
		//request.ParseForm();
		//io.WriteString(w, request.Form["in"][0])
		io.WriteString(w, request.FormValue("in"))
	}
}

func main() {
	http.HandleFunc("/test1", SimpleServer)
	http.HandleFunc("/test2", FormServer)
	if err := http.ListenAndServe(":8088", nil); err != nil {
		panic(err)
	}
}
注:當使用字符串常量表示 html 文本的時候,包含 <html><body>...</body></html> 對於讓瀏覽器將它識別爲 html 文檔非常重要。

更安全的做法是在處理函數中,在寫入返回內容之前將頭部的 content-type 設置爲text/html:w.Header().Set("Content-Type", "text/html")。

content-type 會讓瀏覽器認爲它可以使用函數 http.DetectContentType([]byte(form)) 來處理收到的數據。
package main

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

type statistics struct {
	numbers []float64
	mean    float64
	median  float64
}

const form = `<html><body><form action="/" method="POST">
<label for="numbers">Numbers (comma or space-separated):</label><br>
<input type="text" name="numbers" size="30"><br />
<input type="submit" value="Calculate">
</form></html></body>`

const error = `<p class="error">%s</p>`

var pageTop = ""
var pageBottom = ""

func main() {
	http.HandleFunc("/", homePage)
	if err := http.ListenAndServe(":9001", nil); err != nil {
		log.Fatal("failed to start server", err)
	}
}

func homePage(writer http.ResponseWriter, request *http.Request) {
	writer.Header().Set("Content-Type", "text/html")
	err := request.ParseForm() // Must be called before writing response
	fmt.Fprint(writer, pageTop, form)
	if err != nil {
		fmt.Fprintf(writer, error, err)
	} else {
		if numbers, message, ok := processRequest(request); ok {
			stats := getStats(numbers)
			fmt.Fprint(writer, formatStats(stats))
		} else if message != "" {
			fmt.Fprintf(writer, error, message)
		}
	}
	fmt.Fprint(writer, pageBottom)
}

func processRequest(request *http.Request) ([]float64, string, bool) {
	var numbers []float64
	var text string
	if slice, found := request.Form["numbers"]; found && len(slice) > 0 {
		//處理如果網頁中輸入的是中文逗號
		if strings.Contains(slice[0], "&#65292") {
			text = strings.Replace(slice[0], "&#65292;", " ", -1)
		} else {
			text = strings.Replace(slice[0], ",", " ", -1)
		}
		for _, field := range strings.Fields(text) {
			if x, err := strconv.ParseFloat(field, 64); err != nil {
				return numbers, "'" + field + "' is invalid", false
			} else {
				numbers = append(numbers, x)
			}
		}
	}
	if len(numbers) == 0 {
		return numbers, "", false // no data first time form is shown
	}
	return numbers, "", true
}

func getStats(numbers []float64) (stats statistics) {
	stats.numbers = numbers
	sort.Float64s(stats.numbers)
	stats.mean = sum(numbers) / float64(len(numbers))
	stats.median = median(numbers)
	return
}

func sum(numbers []float64) (total float64) {
	for _, x := range numbers {
		total += x
	}
	return
}

func median(numbers []float64) float64 {
	middle := len(numbers) / 2
	result := numbers[middle]
	if len(numbers)%2 == 0 {
		result = (result + numbers[middle-1]) / 2
	}
	return result
}

func formatStats(stats statistics) string {
	return fmt.Sprintf(`<table border="1">
<tr><th colspan="2">Results</th></tr>
<tr><td>Numbers</td><td>%v</td></tr>
<tr><td>Count</td><td>%d</td></tr>
<tr><td>Mean</td><td>%f</td></tr>
<tr><td>Median</td><td>%f</td></tr>
</table>`, stats.numbers, len(stats.numbers), stats.mean, stats.median)
}
  • 確保網頁應用健壯
    當網頁應用的處理函數發生 panic,服務器會簡單地終止運行。這可不妙:網頁服務器必須是足夠健壯的程序,能夠承受任何可能的突發問題。
    首先能想到的是在每個處理函數中使用 defer/recover,不過這樣會產生太多的重複代碼。使用閉包的錯誤處理模式是更優雅的方案。我們把這種機制應用到前一章的簡單網頁服務器上。實際上,它可以被簡單地應用到任何網頁服務器程序中。
    爲增強代碼可讀性,我們爲頁面處理函數創建一個類型:
    type HandleFnc func(http.ResponseWriter, *http.Request)
    我們的錯誤處理函數 logPanics 函數:
func logPanics(function HandleFnc) HandleFnc {
	return func(writer http.ResponseWriter, request *http.Request) {
		defer func() {
			if x := recover(); x != nil {
				log.Printf("[%v] caught panic: %v", request.RemoteAddr, x)
			}
		}()
		function(writer, request)
	}
}
然後我們用 logPanics 來包裝對處理函數的調用:

http.HandleFunc("/test1", logPanics(SimpleServer))
http.HandleFunc("/test2", logPanics(FormServer))
處理函數現在可以恢復 panic 調用,類似的錯誤檢測函數

完整的例子:
package main

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

const form = `<html><body><form action="#" method="post" name="bar">
		<input type="text" name="in"/>
		<input type="submit" value="Submit"/>
	</form></html></body>`

type HandleFnc func(http.ResponseWriter, *http.Request)

/* handle a simple get request */
func SimpleServer(w http.ResponseWriter, request *http.Request) {
	io.WriteString(w, "<h1>hello, world</h1>")
}

/* handle a form, both the GET which displays the form
   and the POST which processes it.*/
func FormServer(w http.ResponseWriter, request *http.Request) {
	w.Header().Set("Content-Type", "text/html")
	switch request.Method {
	case "GET":
		/* display the form to the user */
		io.WriteString(w, form)
	case "POST":
		/* handle the form data, note that ParseForm must
		   be called before we can extract form data*/
		//request.ParseForm();
		//io.WriteString(w, request.Form["in"][0])
		io.WriteString(w, request.FormValue("in"))
	}
}

func main() {
	http.HandleFunc("/test1", logPanics(SimpleServer))
	http.HandleFunc("/test2", logPanics(FormServer))
	if err := http.ListenAndServe(":8088", nil); err != nil {
		panic(err)
	}
}

func logPanics(function HandleFnc) HandleFnc {
	return func(writer http.ResponseWriter, request *http.Request) {
		defer func() {
			if x := recover(); x != nil {
				log.Printf("[%v] caught panic: %v", request.RemoteAddr, x)
			}
		}()
		function(writer, request)
	}
}
  • 探索 template 包
    我們使用 template 對象把數據結構整合到 HTML 模板中。這項技術確實對網頁應用程序非常有用,然而模板是一項更爲通用的技術方案:數據驅動的模板被創建出來,以生成文本輸出。HTML 僅是其中的一種特定使用案例。
    模板通過與數據結構的整合來生成,通常爲結構體或其切片。當數據項傳遞給 tmpl.Execute() ,它用其中的元素進行替換, 動態地重寫某一小段文本。只有被導出的數據項纔可以被整合進模板中。可以在 {{ 和 }} 中加入數據求值或控制結構。數據項可以是值或指針,接口隱藏了他們的差異。
  • 字段替換:{{.FieldName}}
    要在模板中包含某個字段的內容,使用雙花括號括起以點(.)開頭的字段名。例如,假設 Name 是某個結構體的字段,其值要在被模板整合時替換,則在模板中使用文本 {{.Name}}。當 Name 是 map 的鍵時這麼做也是可行的。要創建一個新的 Template 對象,調用 template.New,其字符串參數可以指定模板的名稱。正如 出現過的,Parse 方法通過解析模板定義字符串,生成模板的內部表示。當使用包含模板定義字符串的文件時,將文件路徑傳遞給 ParseFiles 來解析。解析過程如產生錯誤,這兩個函數第二個返回值 error != nil。最後通過 Execute 方法,數據結構中的內容與模板整合,並將結果寫入方法的第一個參數中,其類型爲 io.Writer。再一次地,可能會有 error 返回。以下程序演示了這些步驟,輸出通過 os.Stdout 被寫到控制檯。
package main

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

type Person struct {
	Name string
	nonExportedAgeField string
}

func main() {
	t := template.New("hello")
	t, _ = t.Parse("hello {{.Name}}!")
	p := Person{Name: "Mary", nonExportedAgeField: "31"}
	if err := t.Execute(os.Stdout, p); err != nil {
		fmt.Println("There was an error:", err.Error())
	}
}
  • 驗證模板格式
    爲了確保模板定義語法是正確的,使用 Must 函數處理 Parse 的返回結果。在下面的例子中 tOK 是正確的模板, tErr 驗證時發生錯誤,會導致運行時 panic
package main

import (
	"text/template"
	"fmt"
)

func main() {
	tOk := template.New("ok")
	//a valid template, so no panic with Must:
	template.Must(tOk.Parse("/* and a comment */ some static text: {{ .Name }}"))
	fmt.Println("The first one parsed OK.")
	fmt.Println("The next one ought to fail.")
	tErr := template.New("error_template")
	template.Must(tErr.Parse(" some static text {{ .Name }"))
}
  • If-else
    運行 Execute 產生的結果來自模板的輸出,它包含靜態文本,以及被 {{}} 包裹的稱之爲管道的文本。例如,運行這段代碼
package main

import (
	"os"
	"text/template"
)

func main() {
	tEmpty := template.New("template test")
	tEmpty = template.Must(tEmpty.Parse("Empty pipeline if demo: {{if ``}} Will not print. {{end}}\n")) //empty pipeline following if
	tEmpty.Execute(os.Stdout, nil)

	tWithValue := template.New("template test")
	tWithValue = template.Must(tWithValue.Parse("Non empty pipeline if demo: {{if `anything`}} Will print. {{end}}\n")) //non empty pipeline following if condition
	tWithValue.Execute(os.Stdout, nil)

	tIfElse := template.New("template test")
	tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} Print IF part. {{else}} Print ELSE part.{{end}}\n")) //non empty pipeline following if condition
	tIfElse.Execute(os.Stdout, nil)
}
  • 點號和 with-end
    點號(.)可以在 Go 模板中使用:其值 {{.}} 被設置爲當前管道的值。
    with 語句將點號設爲管道的值。如果管道是空的,那麼不管 with-end 塊之間有什麼,都會被忽略。在被嵌套時,點號根據最近的作用域取得值。以下程序演示了這點:
package main

import (
	"os"
	"text/template"
)

func main() {
	t := template.New("test")
	t, _ = t.Parse("{{with `hello`}}{{.}}{{end}}!\n")
	t.Execute(os.Stdout, nil)

	t, _ = t.Parse("{{with `hello`}}{{.}} {{with `Mary`}}{{.}}{{end}}{{end}}!\n")
	t.Execute(os.Stdout, nil)
}
  • 模板變量 $
    可以在模板內爲管道設置本地變量,變量名以 $ 符號作爲前綴。變量名只能包含字母、數字和下劃線。以下示例使用了多種形式的有效變量名。
package main

import (
	"os"
	"text/template"
)

func main() {
	t := template.New("test")
	t = template.Must(t.Parse("{{with $3 := `hello`}}{{$3}}{{end}}!\n"))
	t.Execute(os.Stdout, nil)

	t = template.Must(t.Parse("{{with $x3 := `hola`}}{{$x3}}{{end}}!\n"))
	t.Execute(os.Stdout, nil)

	t = template.Must(t.Parse("{{with $x_1 := `hey`}}{{$x_1}} {{.}} {{$x_1}}{{end}}!\n"))
	t.Execute(os.Stdout, nil)
}
  • range-end
    range-end 結構格式爲:{{range pipeline}} T1 {{else}} T0 {{end}}。
    range 被用於在集合上迭代:管道的值必須是數組、切片或 map。如果管道的值長度爲零,點號的值不受影響,且執行 T0;否則,點號被設置爲數組、切片或 map 內元素的值,並執行 T1。
    如果模板爲:
    {{range .}}
    {{.}}
    {{end}}
    那麼執行代碼:
    s := []int{1,2,3,4}
    t.Execute(os.Stdout, s)
    會輸出:
    1
    2
    3
    4
  • 模板預定義函數
    模板代碼中使用的預定義函數,例如 printf 函數工作方式類似於 fmt.Sprintf
package main

import (
	"os"
	"text/template"
)

func main() {
	t := template.New("test")
	t = template.Must(t.Parse("{{with $x := `hello`}}{{printf `%s %s` $x `Mary`}}{{end}}!\n"))
	t.Execute(os.Stdout, nil)
}
  • 用 rpc 實現遠程過程調用
    Go 程序之間可以使用 net/rpc 包實現相互通信,這是另一種客戶端-服務器應用場景。它提供了一種方便的途徑,通過網絡連接調用遠程函數。當然,僅當程序運行在不同機器上時,這項技術才實用。rpc 包建立在 gob 包之上,實現了自動編碼/解碼傳輸的跨網絡方法調用。
    服務器端需要註冊一個對象實例,與其類型名一起,使之成爲一項可見的服務:它允許遠程客戶端跨越網絡或其他 I/O 連接訪問此對象已導出的方法。總之就是在網絡上暴露類型的方法。
    rpc 包使用了 http 和 tcp 協議,以及用於數據傳輸的 gob 包。服務器端可以註冊多個不同類型的對象(服務),但同一類型的多個對象會產生錯誤
    服務器端產生一個 rpc_objects.Args 類型的對象 calc,並用 rpc.Register(object) 註冊。調用 HandleHTTP(),然後用 net.Listen 在指定的地址上啓動監聽。也可以按名稱來註冊對象,例如:rpc.RegisterName(“Calculator”, calc)。
    以協程啓動 http.Serve(listener, nil) 後,會爲每一個進入 listener 的 HTTP 連接創建新的服務線程。我們必須用諸如 time.Sleep(1000e9) 來使服務器在一段時間內保持運行狀態。
 rpc_server.go

package main

import (
	"net/http"
	"log"
	"net"
	"net/rpc"
	"time"
	"./rpc_objects"
)

func main() {
	calc := new(rpc_objects.Args)
	rpc.Register(calc)
	rpc.HandleHTTP()
	listener, e := net.Listen("tcp", "localhost:1234")
	if e != nil {
		log.Fatal("Starting RPC-server -listen error:", e)
	}
	go http.Serve(listener, nil)
	time.Sleep(1000e9)
}
  • rpc_client.go
package main

import (
	"fmt"
	"log"
	"net/rpc"
	"./rpc_objects"
)

const serverAddress = "localhost"

func main() {
	client, err := rpc.DialHTTP("tcp", serverAddress + ":1234")
	if err != nil {
		log.Fatal("Error dialing:", err)
	}
	// Synchronous call
	args := &rpc_objects.Args{7, 8}
	var reply int
	err = client.Call("Args.Multiply", args, &reply)
	if err != nil {
		log.Fatal("Args error:", err)
	}
	fmt.Printf("Args: %d * %d = %d", args.N, args.M, reply)
}
  • 基於網絡的通道 netchan
    備註:Go 團隊決定改進並重新打造 netchan 包的現有版本,它已被移至 old/netchan。old/ 目錄用於存放過時的包代碼,它們不會成爲 Go 1.x 的一部分。本節僅出於向後兼容性討論 netchan 包的概念。
    一項和 rpc 密切相關的技術是基於網絡的通道。使用的通道都是本地的,它們僅存在於被執行的機器內存空間中。netchan 包實現了類型安全的網絡化通道:它允許一個通道兩端出現由網絡連接的不同計算機。其實現原理是,在其中一臺機器上將傳輸數據發送到通道中,那麼就可以被另一臺計算機上同類型的通道接收。一個導出器(exporter)會按名稱發佈(一組)通道。導入器(importer)連接到導出的機器,並按名稱導入這些通道。之後,兩臺機器就可按通常的方式來使用通道。網絡通道不是同步的,它們類似於帶緩存的通道。
發送端示例代碼如下:

exp, err := netchan.NewExporter("tcp", "netchanserver.mydomain.com:1234")
if err != nil {
	log.Fatalf("Error making Exporter: %v", err)
}
ch := make(chan myType)
err := exp.Export("sendmyType", ch, netchan.Send)
if err != nil {
	log.Fatalf("Send Error: %v", err)
}
接收端示例代碼如下:

imp, err := netchan.NewImporter("tcp", "netchanserver.mydomain.com:1234")
if err != nil {
	log.Fatalf("Error making Importer: %v", err)
}
ch := make(chan myType)
err = imp.Import("sendmyType", ch, netchan.Receive)
if err != nil {
	log.Fatalf("Receive Error: %v", err)
}
  • 與 websocket 通信
    備註:Go 團隊決定從 Go 1 起,將 websocket 包移出 Go 標準庫,轉移到 code.google.com/p/go 下的子項目 websocket,同時預計近期將做重大更改。
    import “websocket” 這行要改成:
    import websocket “code.google.com/p/go/websocket
    與 http 協議相反,websocket 是通過客戶端與服務器之間的對話,建立的基於單個持久連接的協議。然而在其他方面,其功能幾乎與 http 相同。在示例 1 中,我們有一個典型的 websocket 服務器,他會自啓動並監聽 websocket 客戶端的連入。示例 2演示了 5 秒後會終止的客戶端代碼。當連接到來時,服務器先打印 new connection,當客戶端停止時,服務器打印 EOF => closing connection。
  • 示例1
package main

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

func server(ws *websocket.Conn) {
	fmt.Printf("new connection\n")
	buf := make([]byte, 100)
	for {
		if _, err := ws.Read(buf); err != nil {
			fmt.Printf("%s", err.Error())
			break
		}
	}
	fmt.Printf(" => closing connection\n")
	ws.Close()
}

func main() {
	http.Handle("/websocket", websocket.Handler(server))
	err := http.ListenAndServe(":12345", nil)
	if err != nil {
		panic("ListenAndServe: " + err.Error())
	}
}
  • 示例2
package main

import (
	"fmt"
	"time"
	"websocket"
)

func main() {
	ws, err := websocket.Dial("ws://localhost:12345/websocket", "",
		"http://localhost/")
	if err != nil {
		panic("Dial: " + err.Error())
	}
	go readFromServer(ws)
	time.Sleep(5e9)
    ws.Close()
}

func readFromServer(ws *websocket.Conn) {
	buf := make([]byte, 1000)
	for {
		if _, err := ws.Read(buf); err != nil {
			fmt.Printf("%s\n", err.Error())
			break
		}
	}
}
  • 用 smtp 發送郵件
    smtp 包實現了用於發送郵件的“簡單郵件傳輸協議”(Simple Mail Transfer Protocol)。它有一個 Client 類型,代表一個連接到 SMTP 服務器的客戶端:
    Dial 方法返回一個已連接到 SMTP 服務器的客戶端 Client
    設置 Mail(from,即發件人)和 Rcpt(to,即收件人)
    Data 方法返回一個用於寫入數據的 Writer,這裏利用 buf.WriteTo(wc) 寫入
package main

import (
	"bytes"
	"log"
	"net/smtp"
)

func main() {
	// Connect to the remote SMTP server.
	client, err := smtp.Dial("mail.example.com:25")
	if err != nil {
		log.Fatal(err)
	}
	// Set the sender and recipient.
	client.Mail("[email protected]")
	client.Rcpt("[email protected]")
	// Send the email body.
	wc, err := client.Data()
	if err != nil {
		log.Fatal(err)
	}
	defer wc.Close()
	buf := bytes.NewBufferString("This is the email body.")
	if _, err = buf.WriteTo(wc); err != nil {
		log.Fatal(err)
	}
}
如果需要認證,或有多個收件人時,也可以用 SendMail 函數發送。它連接到地址爲 addr 的服務器;如果可以,切換到 TLS(“傳輸層安全”加密和認證協議),並用 PLAIN 機制認證;然後以 from 作爲發件人,to 作爲收件人列表,msg 作爲郵件內容,發出一封郵件:

func SendMail(addr string, a Auth, from string, to []string, msg []byte) error


package main

import (
	"log"
	"net/smtp"
)

func main() {
	// Set up authentication information.
	auth := smtp.PlainAuth(
		"",
		"[email protected]",
		"password",
		"mail.example.com",
	)
	// Connect to the server, authenticate, set the sender and recipient,
	// and send the email all in one step.
	err := smtp.SendMail(
		"mail.example.com:25",
		auth,
		"[email protected]",
		[]string{"[email protected]"},
		[]byte("This is the email body."),
	)
	if err != nil {
		log.Fatal(err)
	}
}
  • 常見的陷阱與錯誤
  • 永遠不要使用形如 var p*a 聲明變量,這會混淆指針聲明和乘法運算
  • 永遠不要在for循環自身中改變計數器變量
  • 永遠不要在for-range循環中使用一個值去改變自身的值
  • 永遠不要將goto和前置標籤一起使用
  • 永遠不要忘記在函數名後加括號(),尤其調用一個對象的方法或者使用匿名函數啓動一個協程時
  • 永遠不要使用new()一個map,一直使用make
  • 當爲一個類型定義一個String()方法時,不要使用fmt.Print或者類似的代碼
  • 永遠不要忘記當終止緩存寫入時,使用Flush函數
  • 永遠不要忽略錯誤提示,忽略錯誤會導致程序崩潰
  • 不要使用全局變量或者共享內存,這會使併發執行的代碼變得不安全
  • println函數僅僅是用於調試的目的
  • 應該如何做:
  • 使用正確的方式初始化一個元素是切片的映射,例如map[type]slice
  • 一直使用逗號,ok或者checked形式作爲類型斷言
  • 使用一個工廠函數創建並初始化自己定義類型
  • 僅當一個結構體的方法想改變結構體時,使用結構體指針作爲方法的接受者,否則使用一個結構體值類型
  • 最佳實踐指導:
  • 誤用短聲明導致變量覆蓋
var remember bool = false
if something {
    remember := true //錯誤
}
// 使用remember
在此代碼段中,remember變量永遠不會在if語句外面變成true,如果something爲true,由於使用了短聲明:=,if語句內部的新變量remember將覆蓋外面的remember變量,並且該變量的值爲true,但是在if語句外面,變量remember的值變成了false,所以正確的寫法應該是:

if something {
    remember = true
}
此類錯誤也容易在for循環中出現,尤其當函數返回一個具名變量時難於察覺 ,例如以下的代碼段:

func shadow() (err error) {
	x, err := check1() // x是新創建變量,err是被賦值
	if err != nil {
		return // 正確返回err
	}
	if y, err := check2(x); err != nil { // y和if語句中err被創建
		return // if語句中的err覆蓋外面的err,所以錯誤的返回nil!
	} else {
		fmt.Println(y)
	}
	return
}
  • 誤用字符串
    當需要對一個字符串進行頻繁的操作時,謹記在go語言中字符串是不可變的(類似java和c#)。使用諸如a += b形式連接字符串效率低下,尤其在一個循環內部使用這種形式。這會導致大量的內存開銷和拷貝。應該使用一個字符數組代替字符串,將字符串內容寫入一個緩存中。 例如以下的代碼示例:

var b bytes.Buffer
...
for condition {
    b.WriteString(str) // 將字符串str寫入緩存buffer
}
    return b.String()

注意:由於編譯優化和依賴於使用緩存操作的字符串大小,當循環次數大於15時,效率纔會更佳。

  • 發生錯誤時使用defer關閉一個文件
    如果你在一個for循環內部處理一系列文件,你需要使用defer確保文件在處理完畢後被關閉,例如:
for _, file := range files {
    if f, err = os.Open(file); err != nil {
        return
    }
    // 這是錯誤的方式,當循環結束時文件沒有關閉
    defer f.Close()
    // 對文件進行操作
    f.Process(data)
}
但是在循環結尾處的defer沒有執行,所以文件一直沒有關閉!垃圾回收機制可能會自動關閉文件,但是這會產生一個錯誤,更好的做法是:
for _, file := range files {
    if f, err = os.Open(file); err != nil {
        return
    }
    // 對文件進行操作
    f.Process(data)
    // 關閉文件
    f.Close()
 }
 defer僅在函數返回時纔會執行,在循環的結尾或其他一些有限範圍的代碼內不會執行。
  • 何時使用new()和make()
  • 切片、映射和通道,使用make
  • 數組、結構體和所有的值類型,使用new
  • 不需要將一個指向切片的指針傳遞給函數
    切片實際是一個指向潛在數組的指針。我們常常需要把切片作爲一個參數傳遞給函數是因爲:實際就是傳遞一個指向變量的指針,在函數內可以改變這個變量,而不是傳遞數據的拷貝。
    因此應該這樣做:

   func findBiggest( listOfNumbers []int ) int {}
而不是:

   func findBiggest( listOfNumbers *[]int ) int {}

當切片作爲參數傳遞時,切記不要解引用切片。

  • 使用指針指向接口類型
    查看如下程序:nexter是一個接口類型,並且定義了一個next()方法讀取下一字節。函數nextFew將nexter接口作爲參數並讀取接下來的num個字節,並返回一個切片:這是正確做法。但是nextFew2使用一個指向nexter接口類型的指針作爲參數傳遞給函數:當使用next()函數時,系統會給出一個編譯錯誤:*n.next undefined (type nexter has no field or method next)
package main
import (
    "fmt"
)
type nexter interface {
    next() byte
}
func nextFew1(n nexter, num int) []byte {
    var b []byte
    for i:=0; i < num; i++ {
        b[i] = n.next()
    }
    return b
}
func nextFew2(n *nexter, num int) []byte {
    var b []byte
    for i:=0; i < num; i++ {
        b[i] = n.next() // 編譯錯誤:n.next未定義(*nexter類型沒有next成員或next方法)
    }
    return b
}
func main() {
    fmt.Println("Hello World!")
}

永遠不要使用一個指針指向一個接口類型,因爲它已經是一個指針。

使用值類型時誤用指針
將一個值類型作爲一個參數傳遞給函數或者作爲一個方法的接收者,似乎是對內存的濫用,因爲值類型一直是傳遞拷貝。但是另一方面,值類型的內存是在棧上分配,內存分配快速且開銷不大。如果你傳遞一個指針,而不是一個值類型,go編譯器大多數情況下會認爲需要創建一個對象,並將對象移動到堆上,所以會導致額外的內存分配:因此當使用指針代替值類型作爲參數傳遞時,我們沒有任何收穫。

  • 誤用協程和通道
    在實際應用中,你不需要併發執行,或者你不需要關注協程和通道的開銷,在大多數情況下,通過棧傳遞參數會更有效率。
    但是,如果你使用break、return或者panic去跳出一個循環,很有可能會導致內存溢出,因爲協程正處理某些事情而被阻塞。在實際代碼中,通常僅需寫一個簡單的過程式循環即可。當且僅當代碼中併發執行非常重要,才使用協程和通道。
  • 閉包和協程的使用
package main

import (
    "fmt"
    "time"
)

var values = [5]int{10, 11, 12, 13, 14}

func main() {
    // 版本A:
    for ix := range values { // ix是索引值
        func() {
            fmt.Print(ix, " ")
        }() // 調用閉包打印每個索引值
    }
    fmt.Println()
    // 版本B: 和A版本類似,但是通過調用閉包作爲一個協程
    for ix := range values {
        go func() {
            fmt.Print(ix, " ")
        }()
    }
    fmt.Println()
    time.Sleep(5e9)
    // 版本C: 正確的處理方式
    for ix := range values {
        go func(ix interface{}) {
            fmt.Print(ix, " ")
        }(ix)
    }
    fmt.Println()
    time.Sleep(5e9)
    // 版本D: 輸出值:
    for ix := range values {
        val := values[ix]
        go func() {
            fmt.Print(val, " ")
        }()
    }
    time.Sleep(1e9)
}
版本A調用閉包5次打印每個索引值,版本B也做相同的事,但是通過協程調用每個閉包。按理說這將執行得更快,因爲閉包是併發執行的。如果我們阻塞足夠多的時間,讓所有協程執行完畢,版本B的輸出是:4 4 4 4 4。爲什麼會這樣?在版本B的循環中,ix變量實際是一個單變量,表示每個數組元素的索引值。因爲這些閉包都只綁定到一個變量,這是一個比較好的方式,當你運行這段代碼時,你將看見每次循環都打印最後一個索引值4,而不是每個元素的索引值。因爲協程可能在循環結束後還沒有開始執行,而此時ix值是4。

版本C的循環寫法纔是正確的:調用每個閉包時將ix作爲參數傳遞給閉包。ix在每次循環時都被重新賦值,並將每個協程的ix放置在棧中,所以當協程最終被執行時,每個索引值對協程都是可用的。注意這裏的輸出可能是0 2 1 3 4或者0 3 1 2 4或者其他類似的序列,這主要取決於每個協程何時開始被執行。

在版本D中,我們輸出這個數組的值,爲什麼版本B不能而版本D可以呢?

因爲版本D中的變量聲明是在循環體內部,所以在每次循環時,這些變量相互之間是不共享的,所以這些變量可以單獨的被每個閉包使用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章