十二、重新站起来:从失败中恢复

每个程序都会遇到错误。你应该为它们做好计划。有时候,处理错误可以像报告错误并退出程序一样简单。但是其他错误可能需要额外的操作。你可能需要关闭打开的文件或网络连接,或者以其他方式清理,这样你的程序就不会留下混乱。
我们知道在C++中对堆区的内存由程序员手动分配手动释放。如果在释放内存之前程序崩溃,那么很有可能内存是没有释放,这样的情况出现多了,会不断消耗内存,进而影响操作系统的性能。
可以通过defer语句去处理这个问题。你可以将defer关键字放在任何普通函数或方法调用之前,Go将延迟(也就是推迟)执行函数调用,直到当前函数退出之后。

使用延迟函数调用从错误中恢复

package main

import "fmt"

func Socialize() {
    defer fmt.Println("Goodbye!")
    fmt.Println("Hello!")
    fmt.Println("Nice weather,eh?")
}

func main() {
    Socialize() 
}
// 第一个函数调用被推迟到Socialize退出后
// Hello!
// Nice weather, eh?
// Goodbye!

“defer”关键字通过使用return关键字确保函数调用发生,即使调用函数提前退出。

使用延迟函数调用确保文件关闭

因为defer关键字可以确保“无论如何”都执行函数调用,所以它通常用于需要运行的代码,即使在出现错误的情况下也是如此。一个常见的例子是在文件打开之后关闭它们。

func OpenEile(fileName string) (*os.File, error) {
    fmt.Println("Opening", fileName)
    return os.Open(fileName)
}
func CloseFile(file *os.File) {
    fmt.Println("Closing file")
    file.Close()
}

func GetFloats(fileName string) ([]float64, error) {
    var numbers []float64
    file,err:=OpenFile(fileName)
    if err!=nil{
        return nil,err
    }
    
    // 将这个移动到刚刚调用OpenFile之后
    // 添加“defer”,这样在GetFlows退出后它也会运行
    defer CloseFile(file)
    scanner:=bufio.NewScanner(file)
    for scanner.Scan() {
        number,err:=strconv.ParseFloat(scanner.Text(),64)
        if err!=nil {
        // 现在,即使这里返回了一个错误,CloseFile仍然会被调用!
        return nill, err // 现在即使这里返回了一个错误,CloseFile任然会被调用!
    }
    numbers=append(numbers,number)
}
// 如果这里返回一个错误,CloseFile任然会被调用!
    if scanner.Err()!=nil{
        return nil, scanner.Err()
    }
    return nunbers,ni1 // 当然如果GetFToats正常完成, 就会调用CloseFile
}

列出目录中的文件

尝试创建一个名为my_directory的目录,它包含两个文件和一个子目录,如右图所示。下面的程序将列出my_directory的内容,指出它包含的每个项的名称,以及它是文件还是子目录。
尝试创建一个名为my_directory的目录,它包含两个文件和一个子目录,如右图所示。下面的程序将列出my_directory的内容,指出它包含的每个项的名称,以及它是文件还是子目录。

import (
    "fmt"
    "io/ioutill"
    "log"
)

func main() {
    file,err:=ioutill.ReadDir("my_directory")
    if err != nill {
        log.Fatal(err)
    }
    
    for _, file := range files {
        if file.IsDir() {
            fmt.Println("Directory:", file.Name())
        } else {
            fmt.Println("File:", file.Name())
        }
    }
}

列出子目录中的文件(递归)

package main

import (
    "fmt"
    "io/ioutill"
    "log"
    "path/filepath"
)

// 递归函数,它接受要扫描的路径,返回遇到的任何错误
func scanDirectory(path string) error {
    fmt.Println(path) // 打印当前目录
    // 获取包含内容的切片
    file, err := ioutill.ReadDir(path)
    if err != nil {
        return err
    }
    for _, file := range files {
        filePath := filepath.Join(path, file.Name())
        if file.IsDir() {
            err := scanDirectory(filePath)
            if err != nil {
                 return err
            }
        } else {
            fmt.Println(filePath)
        }
    }
    return ni;
}
func main() {
    err := scanDirectory("go")
    if err != nil {
        log.Fatal(err)
    }
}

递归函数中的错误处理

发起一个panic

当程序出现panic时,当前函数停止运行,程序打印日志消息并崩溃。你可以通过简单地调用内置的panic函数来引发panic。

延迟调用在崩溃前完成

当程序出现panic时,所有延迟的函数调用仍然会被执行。如果有多个延迟调用,它们的执行顺序将与被延迟的顺序相反。

func main() {
    one()
}
func one() {
    defer fmt.Println("deferred in one()")
    two()
}
func two() {
    defer fmt.Println("deferred in two()")
    painc("Let's see what's been deferred!")
}
// deferred in two()
// deferred in one()
// panic: Let's see what's been deferred!
package main

import (
    "fmt"
    "io/ioutill"
    "log"
    "path/filepath"
)

// 递归函数,它接受要扫描的路径,返回遇到的任何错误
func scanDirectory(path string) {
    fmt.Println(path) // 打印当前目录
    // 获取包含内容的切片
    file, err := ioutill.ReadDir(path)
    if err != nil {
        panic(err) // 不返回错误值,而是将其传递给panic
    }
    for _, file := range files {
        filePath := filepath.Join(path, file.Name())
        if file.IsDir() {
             scanDirectory(filePath)
        } else {
            fmt.Println(filePath)
        }
    }
}

func main() {
    scanDirectory("go") // 不再需要存储或检查错误返回值
}

现在,当scanDirectory在读取目录遇到错误时,它就产生panic。所有scanDirectory的递归调用都退出。
无法访问的文件、网络故障和错误的用户输入通常应该被认为是“正常的”,应该通过错误值来进行适当的处理。通常,调用panic应该留给“不可能的”情况:错误表示的是程序中的错误,而不是用户方面的错误。

recover函数

Go提供了一个内置的recover函数,可以阻止程序陷入panic。我们需要使用它来体面地退出程序。在正常程序执行过程中调用recover时,它只返回nil,而不执行其他操作;如果在程序处于panic状态时调用recover,它将停止panic。但是当你在函数中调用panic时,该函数将停止执行。因此,在panic所在的同一函数中调用recover没有意义,因为panic无论如何都会继续:

func freakOut() {
    painc("oh no")
    recover() // 永远不会运行
}

但是,当程序陷入panic时,有一种方法可以调用recover……在panic期间,任何延迟的函数调用都将完成。因此,可以在一个单独的函数中放置一个recover调用,并在引发panic的代码之前使用defer调用该函数。

func calmDown() {
    recover()
}
func freakOut() {
    defer calmDown()
    panic("oh no")
}
func main() {
    freakOut()
    fmt.Println("Exiting normally")
}
// 调用recover不会导致在出现panic时恢复执行,至少不会完全恢复。产生panic的函数将立
// 即返回,而该函数块中panic之后的任何代码都不会执行。但是,在产生panic的函数返回之后,正常的执行将恢复。
func calmDown() {
    recover()
}
func freakOut() {
    defer calmDown()
    panic("oh no") // 当恢复时,freakOut在这个位置返回
    fmt.Println("I won't be rnu!")
}
func main() {
    freakOut()
    fmt.Println("Exiting normally") // 这段代码在freakOut返回之后运行
}

panic值从recover中返回

在介绍panic函数时,我们提到了其参数的类型是interface{},即空接口,因此panic可以接受任何值。同样,recover的返回值的类型也是interface{}。你可以将recover的返回值传递给诸如Println(它接受interface{}值)之类的fmt函数,但是你不能直接对其调用方法。
下面是一些将error值传递给panic的代码。但是在这样做时,error被转换为一个interface{}值。当延迟的函数稍后调用recover时,返回的是interface{}值。因此,即使底层的error值有一个Error方法,试图调用interface{}值上的Error会导致编译错误。

func calmDown() {
    p:=recover() // 返回一个interface{}值
    fmt.Println(p.Error()) // 即使底层的“error”值有一个Error方法,但interface{}值没有, 编译错误
}
func main() {
    defer calmDown()
    err:=fmt.Error("there's an error")
    panic(err) // 将错误值而不是字符串传递给“panic”
}

要对panic值调用方法或执行其他操作,需要使用类型断言将其转换回其底层类型。

func calmDown() {
    p:=recover()
    err, ok := p.(error) // 断言panic值的类型为“error”
    if ok {
        fmt.Println(err.Error()) // 现在有了一个error值,我们可以调用Error方法
    }
}
func main() {
    defer calmDown()
    err := fmt.Errorf("there's an error")
    panic(err) // there's an error
}

从scanDirectory中的panic恢复

在scanDirectory函数中添加了一个对panic的调用来清除错误处理代码,但它也导致程序崩溃。我们可以使用到目前为止学到的关于defer、panic和recover的所有知识,来打印错误信息,并体面地退出程序。我们通过添加一个reportPanic函数来实现这一点,我们将在main中使用defer调用它。我们在调用scanDirectory之前调用它,这可能会引起潜在的panic。
在reportPanic中,我们调用recover并存储它返回的panic值。如果程序处于panic状态,这将会停止panic。

package main

import (
    "fmt"
    "io/ioutill"
    "path/filepath"
)
func reportPanic() {
    p:=recover() // 调用recover并存储它的返回值
    // 如果“”recover返回nil,则没有panic
    if p==nil {
        return // 所以什么也不做
    }
    err, ok := p.(error) // 否则获取底层的“error”值
    if ok {
        fmt.Println(err) // 然后打印出来
    }
}

func scanDirectory(path string) {
    fmt.Println(path)
    files,err:=ioutill.ReadDir(path)
    if err != nil {
        panic(err)
    }
    for _, file := range files {
        filePath := filePath.Join(path,file.Name())
        if file.IsDir() {
            scanDirectory(filePath)
        } else {
            fmt.Println(filePath)
        }
    }
}
func main() {
    // 在调用可能引起panic的代码之前,延迟调用新的reportPanic函数
    defer reportPanic()
    scanDirectory("go")
}

恢复panic

reportPanic还有一个潜在的问题需要解决。现在,它可以拦截任何panic,即使不是来自scanDirectory。如果panic值不能转换为error类型,reportPanic将不会打印它。
我们可以通过在main中使用一个string参数来添加另一个对panic的调用来进行测试:

func main() {
    defer reportPanic()
    panic("some other issue") // 引入一个带有字符串panic值的新panic
    scanDirectory("go")
}

reportPanic函数从新的panic中恢复,但是因为panic值不是一个error,所以reportPanic不会打印它。我们的用户不知道为什么程序失败了!
下边的代码更新了reportPanic以处理未预料到的panic。如果将panic值转换为error的类型断言成功,我们只需像以前那样打印它。但如果失败了,我们只需用同样的panic值再次调用panic。

func reportPanic() {
    p:=recover()
    if p == nil {
        return
    }
    err,ok:=p.(error)
    if ok {
        fmt.Println(err)
    } else {
    // 如果panic值不是error,则使用相同的值恢复panic
        panic(p)
    }
}
十三、分享工作:goroutine和channel

下面的程序使用net/http包连接到一个站点,并通过几个函数调用检索一个Web页面。
http.Response对象是一个struct,其Body字段表示页面的内容。Body满足io包的ReadCloser接口,这意味着它有一个Read方法(允许我们读取页面数据)和一个Close方法(在完成时释放网络连接)。
我们将延迟调用Close,这样在完成读取之后连接会被释放。然后我们将响应体传递给ioutil包的ReadAll函数,该函数将读取其全部内容并将其作为byte值的切片返回。

package main

import (
    "fmt"
    "io/ioutill"
    "log"
    "net/http"
)
func main() {
    response,err:=http.Get("http://example.com")
    if err!=nil {
        log.Fatal(err)
    }
    // 一旦main函数退出,就释放网络连接
    defer response.Body.Close() 
    // 读取响应中的所有数据
    body,err:=ioutill.ReadAll(response.Body)
    if err != nil {
        log.Fatal(err)
    }
    // 将数据转换为字符串并打印
    fmt.Println(string(body))
}

byte类型
它是Go的基本类型(如float64或bool)之一,用于保存原始数据,比如你可能从文件或网络连接中读取的数据。如果直接打印byte值切片,它不会显示任何有意义的内容,但是如果将byte值切片转换为string,则会返回可读文本。

使用goroutine的并发性

当responseSize调用http.Get时,程序必须在那里等待远程网站的响应。它在等待的时候没有做任何有用的事情。
在Go中,并发任务称为goroutine。其他编程语言有一个类似的概念,叫作线程,但是goroutine比线程需要更少的计算机内存,启动和停止的时间更少,这意味着你可以同时运行更多的goroutine。
要启动一个goroutine,可以使用go语句,它只是一个普通的函数或方法调用,前面有go关键字:

go myFunction()
go otherFunction("argument")

编译器阻止你尝试从使用go语句调用的函数中获取返回值。
但是goroutine之间有一种交流方式:channel。channel不仅允许你将值从一个goroutine发送到另一个goroutine,还确保在接收的goroutine尝试使用该值之前,发送的goroutine已经发送了该值。
使用channel的唯一实际方法是从一个goroutine到另一个goroutine的通信。所以为了演示channel,我们需要做一些事情:

  • 创建一个channel。
  • 编写一个函数,该函数接收一个channel作为参数。我们将在一个单独的goroutine中运行这个函数,并使用它通过channel发送值。
  • 在初始的goroutine中接收发送的值。
var myChannel chan float64
var myChannel chan float64
myChannel = make(chan float64)
myChannel :=make(chan float64)

使用channel发送和接收值

// 要在channel上发送值,可以使用<-运算符
myChannel <- 3.14
// 使用<-运算符来接收来自channel的值
<- myChannel
// 将channel作为参数
func greeting (myChannel chan string) {
    myChannel <- "hi" // 通过channel发送一个值
}
func main() {
    // 创建一个新的channel
    myChannel:=make(chan string)
    // 将channel传递给在新goroutine中运行的函数
    go greeting(myChannel)
    // 从channel接收值
    fmt.Println(<-myChannel) // hi
}

goroutine每次向myChanne发送一个值时都会阻塞,直到main goroutine接收到它为止。
main goroutine成为多个goroutine的协调器,只有当它准备读取它们发送的值时,才允许它们继续。
发送操作阻塞了greeting goroutine,直到main goroutine接收到该值。

使用channel修复我们的网页大小程序

我们的报告网页大小的程序仍然有两个问题:

  • 我们不能在go语句中使用responseSize函数的返回值。
  • 在接收到响应大小之前,我们的main goroutine已经完成,因此我
    们添加了一个对time.Sleep的5秒钟的调用。但是5秒有时太长,有时太短。
packag main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)
// 向responseSize传递一个channel,以便它发送页面大小
func responseSize(url string, channel chan int) {
    fmt.Println("Getting",url)
    response,err:=http.Get(url)
    if err!=nil {
        log.Fatal(err)
    }
    defer response.Body.Close()
    body,err:=ioutil.ReadAll(response.Body)
    if err!=nil {
        log.Fatal(err)
    }
    // 不返回页面大小,而是通过channel发送
    channel <- len(body)
}
fun main() {
    size:=make(char int)
    // 每次调用responseSize时,都将channel传递过去
    go responseSize("http://example.com/",size)
    go responseSize("http://example.com/",size)
    go responseSize("http://example.com/",size)
    // channel上将有三个发送,所以也要做三个接收
    fmt.Println(<-size)
    fmt.Println(<-size)
    fmt.Println(<-size)
}

可以对main进一步优化

func main() {
    size:=make(chan int)
    urls := []string{"https://example.com/","http://golang.org","https://golang.org/doc"}
    for _,url := range urls {
        go reponseSize(url,size)
    }
    for i:=0;i<len(urls);i++ {
        fmt.Println(<-size)
    }
}

更新我们的channel以携带一个struct

为了让发送的channel包含更多的信息。我们可以创建一个struct类型,它可以通过channel将两者一起发送。

type Page struct {
    URL string
    Size int
}
func responseSize(url string, channel chan Page) {
    channel<-Page{URL: url,Size: len(body)}
}
func main() {
    pages:=make(chan Page)
    urls := []string{"https://example.com/","http://golang.org","https://golang.org/doc"}
    for _,url := range urls {
        go reponseSize(url,pages)
    }
    for i:=0;i<len(urls);i++ {
        page:=<-pages // 接收Page
        fmt.Printf("%s: %d\n",page.URL,page.Size)
    }
}
十四、响应请求:Web应用程序

下面是一个使用net/http为浏览器提供简单响应的程序。

package main

import (
    "log"
    "net/http"
)
func viewHandler(writer http.ResponseWritter,request *http.Request) {
    message:=[]byte("Hello,Web!")
    _,err:=writter.Write(message) // 向响应中添加“Hello,web!”
    if err!=nil {
        log.Fatal(err)
    }
}
func main() {
    // 如果收到一个以“/hello”结尾的URL请求,那么调用viewHandler函数来生成响应
    http.HandleFunc("/hello",viewHandler)
    // 调用http.ListenAndServe,它启动Web服务器,监听浏览器的请求,并对其做出响应
    err:=http.ListenAndServer("localhost:8080",nil) // 第二个参数中的nil值只表示将使用通过HandleFunc设置的函数来处理请求。
    log.Fatal(err)
}
go run hello.go

http://localhost:8080/hello

你的计算机在自言自语

当启动我们的小Web应用程序时,它启动了自己的Web服务器,就在你的计算机上。因为应用程序是在你的计算机上运行的(而不是在互联网上的某个地方),所以我们在URL中使用特殊的主机名localhost。这告诉浏览器,它需要建立从你的计算机到同一台计算机的连接。
在我们的代码中,我们指定服务器应该监听端口8080,因此我们将其包含在URL中,就在主机名后面。

讲解简单的web应用程序

因为ListenAndServe将永远运行,除非遇到错误。如果是这样,它将返回该错误,在程序退出之前我们将记录该错误。但是,如果没有错误,该程序将继续运行,直到我们在终端按Ctrl-C来中断它。
与main相比,viewHandler函数没有什么特别之处。服务器向viewHandler传递一个http.ResponseWriter,用于向浏览器响应写入数据,以及一个指向http.Request值的指针,该值表示浏览器的请求。
在viewHandler中,我们通过调用ResponseWriter上的Write方法向响应添加数据。Write不接受字符串,但它接受byte值的切片,因此我们将"Hello,web!"字符串转换为[]byte,然后将其传递给Write。
ResponseWriter的Write方法返回成功写入的字节数,以及遇到的任何错误。我们不能对写入的字节数做任何有用的事情,所以我们忽略它。但如果出现错误,我们会将其记录下来并退出程序。

要遵循的模式:HTML模板

我们将为一个网站建立一个简单的留言簿应用程序。访问者将能够以表单的形式输入信息,该表单将被保存到文件中。他们还可以查看之前所有签名的列表。

package main

import (
    "log"
    "net/http"
)
// 将报告错误的代码转移到此函数
func check(err error) {
    if err!=nil {
        log.Fatal(err)
    }
}
func viewHandler(writer http.ResponseWriter, request *http.Request) {
    placeholder:=[]byte("signature list goes here")
    _,err:=write.Write(placeholder) // 响应
    check(err)
}
func main() {
    http.HandleFunc("/guestbook",viewHandler)
    // 设置服务器地址和监听端口
    err:=http.ListenAndServer("localhost:8080",nil)
    log.Fatal(err)
}

Go提供了一个html/template包,它将从文件加载HTML,并为我们插入签名。

import (
    "html/template"
    "log"
    "net/http"
)
func viewHandler(writer http.ResponseWriter, request *http.Request) {
    // 使用view.html的内容创建一个新的模板
    html,err:=template.ParseFiles("view.html") 
    check(err)
    // 将模板的内容写入ResponseWriter
    err=html.Execute(writer,nil)
    check(err)
}

html/template包基于text/template包。使用这两个包的方法几乎完全相同,但是html/template有一些额外的安全特性,这是使用HTML所需要的。

package main

import(
    "log"
    "os"
    "text/template"
)
func check(err error) {
    if err!=nil {
        log.Fatal(err)
    }
}
func main() {
    text:="Here's my template!\n"
    tmpl,err:=template.New("test").Parse(text) // 基于文本创建一个新的Template值
    check(err)
    // 将模板写入终端,而不是HTTP响应
    err=tmpl.Execute(os.Stdout,nil) // Here's my template
    check(err)
}

http.ResponseWriter值和os.Stdout都满足io.Writer接口,并可以传递给Template值的Execute方法。Execute将通过对传递给它的任何值调用Write方法来写出模板。

使用action将数据插入模板

Template值的Execute方法的第二个参数允许你传入要插入到模板的数据。它的类型是空接口,这意味着你可以传入任何类型的值。
要在模板中插入数据,可以向模板文本添加action(操作)。action用双花括号{{}}表示。在双花括号中,指定要插入的数据或要模板执行的操作。每当模板遇到action时,它将计算其内容,并在action的位置将结果插入模板文本中。在一个操作中,你可以使用一个带有“dot”(点)的Execute方法来引用传递给它的数据值。

func main() {
    templateText:="Template start\nAction:{{.}}\nTemplate end\n"
    teml,err:=template.New("test").Parse(templateText)
    check(err)
    err=tml.Execute(os.Stdout,"ABC")
}
// Template start
// Action: ABC

使用“range”action来重复模板的某部分

在{{range}}action与其对应的{{end}}标记之间的模板部分,将对数组、切片、映射或channel中收集的每个值进行重复。该部分中的任何操作也将被重复。
这个模板包含一个{{range}}action,它将输出切片中的每个元素。在循环之前和之后,点的值将是切片本身。但是在循环中,点指的是切片的当前元素。你将在输出中看到这一点。

func executeTemplate(text string, data interface{}) {
    tmpl,err:=template.New("test").Parse(text) // 分析给定文本以创建模板
    check(err)
    err=tmpl.Execute(os.Stdout,data) // 在action中使用给定的数据值
    check(err) 
}
templateText:="Before loop: {{.}}\n{{range .}}In loop: {{.}}\n{{end}}After loop: {{.}}\n"
executeTemplate(templateText,[]string{"do","re","mi"})
// Before loop: [do re mi]
// In loop: do
// In loop: re
// In loop: mi
// After loop: [do re mi]

如果提供给{{range}}action的值为空或nil,则循环根本不会运行。

使用action将struct字段插入模板

type Part struct {
    Name string
    Count int
}
templateText:="Name:{{.Name}}\nCount:{{.Count}}/n"
executeTemplate(templateText,Part{Name:"Fuses,Count: 5"})

保存签名和签名数的struct

type Guestbook struct {
    SignatureCount int
    Signature []string
}
func viewHandler(writer http.ResponseWriter,request *http.Request) {
    signature:=getStrings("signature.txt")
    html,err:=template.ParseFiles("view.html")
    check(err)
    guestbook:=Guestbook{
        SignatureCount:len(signature),
        Signature:signatures,
    }
    err=html.Execute(writer,guest(book))
    check(err)
}

更新模板以包含签名

现在让我们更新view.html中的模板文本以显示签名列表。

<h1>Guestbook</h1>
<div>
    {{,SignatureCount}} total signature - 
    <a href="/guestbook/new">Add Your Signature</a>
</div>
<div>
    {{range .Signature}}
        <p>{{.}}</p>
    {{end}}
</div>

允许用户使用HTML表单添加数据

接下来,我们需要允许访问者加他们自己的签名。我们需要创建一个HTML表单,这样他们可以在其中输入签名。表单通常提供一个或多个用户可以输入数据的字段,以及一个允许用户将数据发送到服务器的提交按钮。
在项目目录中,使用下面的HTML代码创建一个名为new.html的文件。这里有一些我们以前没有见过的标记.

<h1>Add a Signature</h1>

<from>
    <div><input type="text" name="signature"></div>
    <div><input type="submit"></div>
</from>

我们已经在view.html中有一个“Add Your Signature”链接,其指向/guestbook/new路径。单击这个链接将转到同一服务器上的新路径,就像输入这个URL:http://localhost:8080/guestbook/new。
为了让服务器提供新访问的内容,增加以下内容。

func newHandler(writer http.ResponseWriter, request *http.Request) {
    // 将new.html的内容作为模板的文本的加载
    html,err:=template.ParseFiles("new.html")
    check(err)
    // 将模板写入响应(不需要在其中插入任何数据)
    err=html.Execute(writer,nil)
    check(err)
}
func main() {
    http.HandlFun("/guestbook",viewHandler)
    // 将newHandler函数设置为处理路径为/guestbook/new的请求
    http.HandlerFunc("/guest/new",newHandler)
    err:=http.ListenAndServe("localhost:8080",nil)
    log.Fatal(err)
}

当有人访问/guestbook/new路径时,无论是直接输入还是单击链接,都会显示用于输入签名的表单。但是,如果你填写了该表单并单击Submit,则不会发生任何事情。

用于表单提交的Path和HTTP方法

提交表单实际上需要向服务器发出两个请求:一个请求获取表单,另一个请求将用户的数据发送回服务器。让我们更新表单的HTML,以指定第二个请求应该发送到何处以及如何发送。
编辑new.html并向from元素添加两个新的HTML属性。第一个属性action将指定用于提交请求的路径。为了不让路径默认返回/guestbook/new,我们将指定一个新的路径:/guestbook/create。

<h1>Add a Signature</h1>
// 将表单数据提交到"/guestbook/create",作为POST提交而不是GET
<from action="/guestbook/create" method="POST">
    <div><input type="text" name="signature"></div>
    <div><input type="submit"></div>
</from>
createHandler(writer http.ResponseWriter, request *http.Request) 
func createHandler(writer http.ResponseWriter, request *http.Request) {
    signature:=request.FormValue("signature")
    options:=os.O_WRONLY|os.O_APPEND|os.O_CREAT
    // 打开文件
    file,err:=os.OpenFile("signature.txt",options,os.FileMode(0600))
    check(err)
    // 在文件新行上写一个签名
    _,err=fmt.Fprintln(file,signature)
    check(err)
    err=file.Close()
    check(err)
    // 重定向到路径
    http.Redirect(writer,request,"/guestbook",http.StatusFound) // 表示请求成的响应代码
}