Golang Web 开发 (一)

主要获益点

1.1 以创纪录的速度跨平台部署

对于企业来说,Go可以提供快速的跨平台部署。通过它的goroutine、本机编译和基于uri的包命名空间,Go代码被编译成一个单一的、小的二进制文件(零依赖),这使得它非常快。

1.2 利用Go的开箱即用性能轻松扩展

  • 编译成一个二进制文件——“使用静态链接,Go实际上根据OS类型和体系结构将所有依赖库和模块组合成一个二进制文件。”
  • 静态类型系统 ——“类型系统对于大规模应用非常重要。”
  • 性能——Go的性能更好,因为它的并发模型和CPU可伸缩性。每当我们需要处理一些内部请求时,我们都会使用单独的goroutine来处理,这比Python线程节省了10倍的资源。”
  • 不需要web框架 ——“在大多数情况下,你真的不需要任何第三方库。”
  • 出色的IDE支持和调试——“在将所有项目重写为Go之后,我们得到的代码比之前少了64%。”

通过例子学习Go Web编程

Go by Example

Tutorials

关于Go编程语言

Go是一种开源编程语言,旨在构建简单、快速和可靠的软件。看看哪些伟大的公司使用Go来增强他们的服务。

Go Web Examples提供了关于如何在Go编程语言中进行Web开发的易于理解的代码片段。它的灵感来自于Go by Example,它对这种语言的基础有很好的介绍。

如果你正在用Go学习web编程,或者刚刚开始学习,你可以在这里找到一些很好的例子和教程。我们的目标是提供非常详细的干净示例,这样你就可以成为下一个Go web开发人员!Go Web Examples涵盖了Web编程的基础知识。从路由器和模板到中间件和websockets。在这里,您可以找到干净的代码片段到详细的教程。

首先,看看如何创建一个经典的“Hello World”web应用程序的第一个例子,或者直接进入路由(使用gorilla/mux路由器)

1、Hello World

1.1 简介

Go已经内置了一个web服务器。来自标准库的net/http包包含关于HTTP协议的所有功能。这包括(许多其他东西)一个HTTP客户端和一个HTTP服务器。在这个例子中,你会发现创建一个可以在浏览器中查看的web服务器是多么简单。

1.2 注册请求处理程序

首先,创建一个Handler来接收来自浏览器、HTTP客户端或API请求的所有传入HTTP连接。Go中的handler 是一个具有以下签名的函数:

func (w http.ResponseWriter, r *http.Request)

函数接收两个参数:
一个http.ResponseWriter就是你写text/html响应的地方。
一个 http.Request,它包含了关于这个HTTP请求的所有信息,包括URL或报头字段(header fields)。
注册一个请求处理程序到默认的HTTP服务器就像这样简单:

扫描二维码关注公众号,回复: 15018108 查看本文章
http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
    
    
    fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
})

1.3 监听HTTP连接

请求处理程序本身不能接受来自外部的任何HTTP连接。HTTP服务器必须监听端口,以便将连接传递给请求处理程序。因为端口80在大多数情况下是HTTP通信的默认端口,所以这个服务器也会监听它

下面的代码将启动Go的默认HTTP服务器,并侦听端口80上的连接。您可以将浏览器导航到http://localhost/,并看到服务器正在处理您的请求。

http.ListenAndServe(":80", nil)

1.4 The Code (for copy/paste)

这是完整的代码,您可以使用它来测试您在本例中所学到的内容。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    
    
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    
    
        fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
    })

    http.ListenAndServe(":80", nil)
}

2、HTTP Server

2.1 简介

在这个例子中,你将学习如何在Go中创建一个基本的HTTP服务器。首先,让我们讨论一下HTTP服务器应该具备哪些功能。基本的HTTP服务器有几个关键任务需要处理。

  • 处理动态请求:处理来自浏览网站、登录帐户或发布图像的用户的传入请求。
  • 提供静态资产:为浏览器提供JavaScript、CSS和图像,为用户创建动态体验。
  • 接受连接:HTTP服务器必须监听一个特定的端口,以便能够接受来自互联网的连接。

2.2 处理动态请求

net/http包包含接受请求和动态处理请求所需的所有实用程序。我们可以注册一个新的处理程序与http。HandleFunc函数。它的第一个参数接受一个要匹配的路径和第二个参数为一个执行的函数。在这个例子中:当有人浏览你的网站(http://example.com/)时,他或她会收到一条很好的消息。

http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
    
    
    fmt.Fprint(w, "Welcome to my website!")
})

对于动态方面,http.Request包含关于请求及其参数的所有信息。你可以用r.URL.Query().Get("token")读取GET参数,或者用r.FormValue("email")读取POST参数(来自HTML表单的字段)。

2.3 服务静态资源

为了提供像JavaScript、CSS和图像这样的静态资源,我们使用了内置的http.FileServer,并将其指向url路径。为了使文件服务器正常工作,它需要知道从哪里提供文件。我们可以这样做:

fs := http.FileServer(http.Dir("static/"))

一旦我们的文件服务器就位,我们只需要将一个url路径指向它,就像我们对动态请求所做的一样。需要注意的一点是:为了正确地提供文件,我们需要去掉url路径的一部分。通常这是我们文件所在目录的名称。

http.Handle("/static/", http.StripPrefix("/static/", fs))

2.4 接受连接

完成基本HTTP服务器的最后一件事是侦听端口以接受来自internet的连接。正如你可以猜到的,Go也有一个内置的HTTP服务器,我们可以快速启动。一旦启动,就可以在浏览器中查看HTTP服务器。

http.ListenAndServe(":80", nil)

The Code

package main

import (
    "fmt"
    "net/http"
)

func main() {
    
    
    http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
    
    
        fmt.Fprintf(w, "Welcome to my website!")
    })

    fs := http.FileServer(http.Dir("static/"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    http.ListenAndServe(":80", nil)
}

3、Routing (using gorilla/mux)

3.1 简介

Go的net/http包为HTTP协议提供了很多功能。它做得不太好的一件事是复杂的请求路由,比如将请求url分割成单个参数。幸运的是,有一个非常受欢迎的包,它在Go社区中以良好的代码质量而闻名。在本例中,您将看到如何使用gorilla/mux创建带有命名参数、GET/POST处理程序和域限制的路由

3.2 安装gorilla/mux包

gorilla/mux一个适应Go默认HTTP路由器的包。当编写web应用程序时,它提供了许多功能来提高工作效率。它还兼容Go的默认请求处理程序签名func (w http.ResponseWriter, r *http.Request),因此该包可以与其他HTTP库(如中间件或现有应用程序)混合和匹配。使用go get命令从GitHub安装包,如下所示:

3.3 新建路由

首先创建一个新的请求路由器。路由器是web应用程序的主路由器,稍后将作为参数传递给服务器。它将接收所有HTTP连接并将其传递给您将在其上注册的请求处理程序。你可以像这样创建一个新的路由器:

r := mux.NewRouter()

3.4 Registering a Request Handler

一旦你有了一个新的路由器,你就可以像往常一样注册请求处理程序。唯一的区别是,不是调用http.HandleFunc(...),而是像这样r.HandleFunc(...)在路由器上调用HandleFunc:

3.5 URL Parameters

gorilla/mux路由器最大的优点是从请求URL中提取参数(segments )的能力。例如,这是应用程序中的一个URL:

/books/go-programming-blueprint/page/10

这个URL有两个动态参数(segments ):

  • 书名段(go-programming-blueprint)
  • Page (10)

为了让一个请求处理程序与上面提到的URL匹配,你可以像这样在你的URL模式中用占位符替换动态段(dynamic segments):

r.HandleFunc("/books/{title}/page/{page}", func(w http.ResponseWriter, r *http.Request) {
    
    
    // get the book
    // navigate to the page
})

最后一件事是从这些片段中获取数据。该包带有函数mux.Vars(r),该函数接受http.Request作为参数,并返回段的map。

func(w http.ResponseWriter, r *http.Request) {
    
    
    vars := mux.Vars(r)
    vars["title"] // the book title slug
    vars["page"] // the page
}

3.6 设置HTTP服务器的路由器

曾经想知道http.ListenAndServe(":80", nil)nil是什么?它是HTTP服务器的主路由器的参数。默认情况下,它是nil,这意味着使用net/http包的默认路由器。要使用您自己的路由器,请将nil替换为路由器r的变量。

http.ListenAndServe(":80", r)

code

package main

import (
    "fmt"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    
    
    r := mux.NewRouter()

    r.HandleFunc("/books/{title}/page/{page}", func(w http.ResponseWriter, r *http.Request) {
    
    
        vars := mux.Vars(r)
        title := vars["title"]
        page := vars["page"]

        fmt.Fprintf(w, "You've requested the book: %s on page %s\n", title, page)
    })

    http.ListenAndServe(":80", r)
}

3.7 gorilla/mux路由器的特性

Methods

将请求处理程序限制为特定的HTTP方法。

r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")

主机名和子域

将请求处理程序限制为特定的主机名或子域。

r.HandleFunc("/books/{title}", BookHandler).Host("www.mybookstore.com")

Schemes

将请求处理程序限制为http/https。

r.HandleFunc("/secure", SecureHandler).Schemes("https")
r.HandleFunc("/insecure", InsecureHandler).Schemes("http")

Path Prefixes & Subrouters

bookrouter := r.PathPrefix("/books").Subrouter()
bookrouter.HandleFunc("/", AllBooks)
bookrouter.HandleFunc("/{title}", GetBook)

4、MySQL Database

4.1 简介

在某个时间点上,您希望web应用程序从数据库中存储和检索数据。在处理动态内容、为用户提供表单以输入数据或存储登录名和密码凭据以供用户进行身份验证时,几乎总是这样。为此,我们有数据库。

数据库有各种各样的形式和形状。一个在所有网络上常用的数据库是MySQL数据库。它已经存在了很长时间,已经证明了它的地位和稳定性,次数之多你都数不清。

在这个例子中,我们将深入研究Go中数据库访问的基本原理,创建数据库表,存储数据并再次检索数据。

4.2 安装go-sql-driver/mysql包

Go编程语言提供了一个名为“database/sql”的方便包,可以查询各种sql数据库。这很有用,因为它将所有常见SQL特性抽象为一个API供您使用。Go不包括的是数据库驱动程序。在Go中,数据库驱动程序是一个包,它实现了特定数据库(在我们的例子中是MySQL)的底层细节。正如您可能已经猜到的那样,这对于保持向前兼容非常有用。因为,在创建所有的Go包时,作者无法预见未来的每一个数据库,支持每一个可能的数据库将会有大量的维护工作。

要安装MySQL数据库驱动程序,转到您选择的终端并运行:

go get -u github.com/go-sql-driver/mysql

4.3 连接MySQL数据库

在安装完所有必要的包后,我们需要检查的第一件事是,我们是否可以成功连接到我们的MySQL数据库。如果你没有运行MySQL数据库服务器,你可以很容易地用Docker启动一个新实例。下面是Docker MySQL镜像的官方文档:https://hub.docker.com/_/mysql

为了检查我们是否可以连接到我们的数据库,导入database/sqlgo-sql-driver/mysql包,并像这样打开一个连接:

import "database/sql"
import _ "go-sql-driver/mysql"


// Configure the database connection (always check errors)
db, err := sql.Open("mysql", "username:password@(127.0.0.1:3306)/dbname?parseTime=true")



// Initialize the first connection to the database, to see if everything works correctly.
// Make sure to check the error.
err := db.Ping()

4.4 创建第一个数据库表

数据库中的每个数据条目都存储在一个特定的表中。数据库表由列和行组成。列给每个数据条目一个标签并指定它的类型。行是插入的数据值。在第一个例子中,我们想创建一个这样的表:

id username password created_at
1 johndoe secret 2019-08-10 12:30:00

翻译成SQL,创建表的命令是这样的:

CREATE TABLE users (
    id INT AUTO_INCREMENT,
    username TEXT NOT NULL,
    password TEXT NOT NULL,
    created_at DATETIME,
    PRIMARY KEY (id)
);

现在我们有了SQL命令,我们可以使用database/sql包在MySQL数据库中创建表:

query := `
    CREATE TABLE users (
        id INT AUTO_INCREMENT,
        username TEXT NOT NULL,
        password TEXT NOT NULL,
        created_at DATETIME,
        PRIMARY KEY (id)
    );`

// Executes the SQL query in our database. Check err to ensure there was no error.
_, err := db.Exec(query)

4.5 Inserting our first user

如果您熟悉SQL,那么向表中插入新数据就像创建表一样简单。需要注意的一点是:默认情况下,Go使用prepared statements将动态数据插入到SQL查询中,这是一种安全地将用户提供的数据传递到数据库的方法,而不会有任何损坏的风险。在web编程的早期,程序员直接将数据与查询一起传递到数据库,这导致了大量的漏洞,并可能破坏整个web应用程序。请不要这样做。这很容易做到。

为了将第一个用户插入到数据库表中,我们创建如下所示的SQL查询。如您所见,我们省略了id列,因为它是由MySQL自动设置的。问号告诉SQL驱动程序,它们是实际数据的占位符。在这里你可以看到我们讨论过的prepared statements

INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)

现在我们可以在Go中使用这个SQL查询,并在我们的表中插入一个新行:

import "time"

username := "johndoe"
password := "secret"
createdAt := time.Now()

// Inserts our data into the users table and returns with the result and a possible error.
// The result contains information about the last inserted id (which was auto-generated for us) and the count of rows this query affected.
result, err := db.Exec(`INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)`, username, password, createdAt)

要获取新创建的用户id,只需像这样获取:

userID, err := result.LastInsertId()

4.6 查询用户表

现在我们的表中有了一个用户,我们想要查询它并取回它的所有信息。在Go中,我们有两种查询表的方法:一个是db.Query,可以查询多行,供我们迭代;一个db.QueryRow,如果我们只想查询特定的行。

查询特定行的工作原理与我们之前介绍过的其他SQL命令基本相同。
通过ID查询单个用户的SQL命令如下所示:

SELECT id, username, password, created_at FROM users WHERE id = ?

在Go中,我们首先声明一些变量来存储数据,然后像这样查询单个数据库行:

var (
    id        int
    username  string
    password  string
    createdAt time.Time
)

// Query the database and scan the values into out variables. Don't forget to check for errors.
query := `SELECT id, username, password, created_at FROM users WHERE id = ?`
err := db.QueryRow(query, 1).Scan(&id, &username, &password, &createdAt)

4.7 Querying all users

在前一节中,我们介绍了如何查询单个用户行。许多应用程序都有需要查询所有现有用户的用例。这与上面的示例类似,但涉及到更多的编码。

我们可以使用上面示例中的SQL命令并删除WHERE子句。通过这种方式,我们可以查询所有现有用户:

SELECT id, username, password, created_at FROM users

在Go中,我们首先声明一些变量来存储数据,然后像这样查询单个数据库行:

type user struct {
    
    
    id        int
    username  string
    password  string
    createdAt time.Time
}

rows, err := db.Query(`SELECT id, username, password, created_at FROM users`) // check err
defer rows.Close()

var users []user
for rows.Next() {
    
    
    var u user
    err := rows.Scan(&u.id, &u.username, &u.password, &u.createdAt) // check err
    users = append(users, u)
}
err := rows.Err() // check err

用户切片现在可能包含如下内容:

users {
    
    
    user {
    
    
        id:        1,
        username:  "johndoe",
        password:  "secret",
        createdAt: time.Time{
    
    wall: 0x0, ext: 63701044325, loc: (*time.Location)(nil)},
    },
    user {
    
    
        id:        2,
        username:  "alice",
        password:  "bob",
        createdAt: time.Time{
    
    wall: 0x0, ext: 63701044622, loc: (*time.Location)(nil)},
    },
}

4.8 从表中删除一个用户

最后,从我们的表中删除一个用户就像上面章节中的.Exec一样简单:

_, err := db.Exec(`DELETE FROM users WHERE id = ?`, 1) // check err

全部代码如下:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    
    
    db, err := sql.Open("mysql", "root:root@(127.0.0.1:3306)/root?parseTime=true")
    if err != nil {
    
    
        log.Fatal(err)
    }
    if err := db.Ping(); err != nil {
    
    
        log.Fatal(err)
    }

    {
    
     // Create a new table
        query := `
            CREATE TABLE users (
                id INT AUTO_INCREMENT,
                username TEXT NOT NULL,
                password TEXT NOT NULL,
                created_at DATETIME,
                PRIMARY KEY (id)
            );`

        if _, err := db.Exec(query); err != nil {
    
    
            log.Fatal(err)
        }
    }

    {
    
     // Insert a new user
        username := "johndoe"
        password := "secret"
        createdAt := time.Now()

        result, err := db.Exec(`INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)`, username, password, createdAt)
        if err != nil {
    
    
            log.Fatal(err)
        }

        id, err := result.LastInsertId()
        fmt.Println(id)
    }

    {
    
     // Query a single user
        var (
            id        int
            username  string
            password  string
            createdAt time.Time
        )

        query := "SELECT id, username, password, created_at FROM users WHERE id = ?"
        if err := db.QueryRow(query, 1).Scan(&id, &username, &password, &createdAt); err != nil {
    
    
            log.Fatal(err)
        }

        fmt.Println(id, username, password, createdAt)
    }

    {
    
     // Query all users
        type user struct {
    
    
            id        int
            username  string
            password  string
            createdAt time.Time
        }

        rows, err := db.Query(`SELECT id, username, password, created_at FROM users`)
        if err != nil {
    
    
            log.Fatal(err)
        }
        defer rows.Close()

        var users []user
        for rows.Next() {
    
    
            var u user

            err := rows.Scan(&u.id, &u.username, &u.password, &u.createdAt)
            if err != nil {
    
    
                log.Fatal(err)
            }
            users = append(users, u)
        }
        if err := rows.Err(); err != nil {
    
    
            log.Fatal(err)
        }

        fmt.Printf("%#v", users)
    }

    {
    
    
        _, err := db.Exec(`DELETE FROM users WHERE id = ?`, 1)
        if err != nil {
    
    
            log.Fatal(err)
        }
    }
}

5、templates

5.1 简介

Go的html/template包为 HTML 模板提供了丰富的模板语言。它主要用于web应用程序,以结构化的方式在客户端浏览器中显示数据Go模板语言的一大优点是自动转义数据。无需担心XSS攻击,因为Go会解析HTML模板并在将其显示给浏览器之前转义所有输入。

5.2 第一个模板

用Go编写模板非常简单。这个例子展示了一个TODO列表,在HTML中以无序列表(ul)的形式编写。**在呈现模板时,传入的数据可以是Go的任何类型的数据结构。**它可以是一个简单的字符串或数字,甚至可以是嵌套的数据结构,如下例所示。要访问模板中的数据,最上面的变量是通过{ {.}}访问。花括号内的点称为管道和数据的根元素。

data := TodoPageData{
    
    
    PageTitle: "My TODO list",
    Todos: []Todo{
    
    
        {
    
    Title: "Task 1", Done: false},
        {
    
    Title: "Task 2", Done: true},
        {
    
    Title: "Task 3", Done: true},
    },
}
<h1>{
    
    {
    
    .PageTitle}}</h1>
<ul>
    {
    
    {
    
    range .Todos}}
        {
    
    {
    
    if .Done}}
            <li class="done">{
    
    {
    
    .Title}}</li>
        {
    
    {
    
    else}}
            <li>{
    
    {
    
    .Title}}</li>
        {
    
    {
    
    end}}
    {
    
    {
    
    end}}
</ul>

5.3 控制结构

模板语言包含一组丰富的控制结构来呈现HTML。在这里,你将得到一个最常用的概述。要获得所有可能结构的详细列表,请访问:text/template

Control Structure Definition
{ {/* a comment */}} Defines a comment
{ {.}} Renders the root element
{ {.Title}} Renders the “Title”-field in a nested element
{ {if .Done}} { {else}} { {end}} Defines an if-Statement
{ {range .Todos}} { {.}} { {end}} Loops over all “Todos” and renders
{ {block “content” .}} { {end}} Defines a block with the name “content”

5.4 从文件解析模板

模板既可以解析字符串,也可以解析磁盘上的文件。通常情况下,模板是来自磁盘的备份,本例展示了如何做到这一点。在本例中,在Go程序所在的目录中有一个模板文件,名为layout.html

tmpl, err := template.ParseFiles("layout.html")
// or
tmpl := template.Must(template.ParseFiles("layout.html"))

5.5 在请求处理程序中执行模板

从磁盘解析模板之后,就可以在请求处理程序中使用它了。Execute函数接受一个io.Writer用于写出模板和一个interface{}来将数据传递到模板。在http.ResponseWriter 调用该函数时。Content-Type 报头在HTTP响应中自动设置为Content-Type: text/html; charset=utf-8

func(w http.ResponseWriter, r *http.Request) {
    
    
    tmpl.Execute(w, "data goes here")
}

The Code

package main


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



type Todo struct {
    
    
    Title string
    Done  bool
}



type TodoPageData struct {
    
    
    PageTitle string
    Todos     []Todo
}



func main() {
    
    
    tmpl := template.Must(template.ParseFiles("layout.html"))
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    
    
        data := TodoPageData{
    
    
            PageTitle: "My TODO list",
            Todos: []Todo{
    
    
                {
    
    Title: "Task 1", Done: false},
                {
    
    Title: "Task 2", Done: true},
                {
    
    Title: "Task 3", Done: true},
            },
        }
        tmpl.Execute(w, data)
    })
    http.ListenAndServe(":80", nil)
}

layout.html如下:

<h1>{
   
   {.PageTitle}}</h1>
<ul>
    {
   
   {range .Todos}}
    {
   
   {if .Done}}
    <li class="done">{
   
   {.Title}}</li>
    {
   
   {else}}
    <li>{
   
   {.Title}}</li>
    {
   
   {end}}
    {
   
   {end}}
</ul><h1>{
   
   {.PageTitle}}</h1>
<ul>
    {
   
   {range .Todos}}
    {
   
   {if .Done}}
    <li class="done">{
   
   {.Title}}</li>
    {
   
   {else}}
    <li>{
   
   {.Title}}</li>
    {
   
   {end}}
    {
   
   {end}}
</ul>

6、Assets and Files

这个例子将展示如何从特定目录提供静态文件,如CSS、JavaScript或图像。

// static-files.go
package main

import "net/http"

func main() {
    
    
    fs := http.FileServer(http.Dir("assets/"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    http.ListenAndServe(":8080", nil)
}
$ tree assets/
assets/
└── css
    └── styles.css
$ go run static-files.go

$ curl -s http://localhost:8080/static/css/styles.css
body {
    
    
    background-color: black;
}

7、表单

此示例将展示如何模拟联系人表单并将消息解析为结构体。

// forms.go
package main

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

type ContactDetails struct {
    
    
    Email   string
    Subject string
    Message string
}

func main() {
    
    
    tmpl := template.Must(template.ParseFiles("forms.html"))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    
    
        if r.Method != http.MethodPost {
    
    
            tmpl.Execute(w, nil)
            return
        }

        details := ContactDetails{
    
    
            Email:   r.FormValue("email"),
            Subject: r.FormValue("subject"),
            Message: r.FormValue("message"),
        }

        // do something with details
        _ = details

        tmpl.Execute(w, struct{
    
     Success bool }{
    
    true})
    })

    http.ListenAndServe(":8080", nil)
}
<!-- forms.html -->
{
   
   {if .Success}}
    <h1>Thanks for your message!</h1>
{
   
   {else}}
    <h1>Contact</h1>
    <form method="POST">
        <label>Email:</label><br />
        <input type="text" name="email"><br />
        <label>Subject:</label><br />
        <input type="text" name="subject"><br />
        <label>Message:</label><br />
        <textarea name="message"></textarea><br />
        <input type="submit">
    </form>
{
   
   {end}}

猜你喜欢

转载自blog.csdn.net/chinusyan/article/details/130091165