Go的研习笔记-day13(以Java的视角学习Go)

网络,模板和网页应用
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中的变量声明是在循环体内部,所以在每次循环时,这些变量相互之间是不共享的,所以这些变量可以单独的被每个闭包使用。
发布了213 篇原创文章 · 获赞 258 · 访问量 28万+

猜你喜欢

转载自blog.csdn.net/wolf_love666/article/details/99622932