从0开始创建一个go web项目

作者: dreamfly 分类: go 发布时间: 2020-04-11 17:59

Go作为构建Web应用程序的首选语言正变得越来越流行。

这在很大程度上要归功于它的速度,应用程序性能以及可移植性。互联网上有很多资源可以教您如何在Go中构建端到端Web应用程序,但是在大多数情况下,它们要么散布在孤立的博客文章中,要么散布在书籍中。

这里将从0开始搭建一个完整的项目。

项目效果

我们将建立鸟类社区百科全书。该网站将:

  • 显示社区提交的不同条目,以及它们找到的鸟的名称和详细信息。
  • 允许任何人张贴关于他们看见的鸟的新条目。

此应用程序将需要三个组件:

  • Web服务器
  • 前端(客户端)应用
  • 数据库

设置环境

配置GOPATH,就是将GOPATH添加到系统的环境变量中去。

设置目录

gopath目录设置三个子目录,没有创建,分别是src,bin,pkg

  • src 所有 Go源代码的去向,包括我们自己要创建的代码
  • bin go编译之后的可执行文件
  • pkg 包含由库制作的包对象(您现在不必担心)

创建项目目录

src目录中的项目文件夹应遵循与远程存储库所在位置相同的位置结构。因此,例如,如果我要创建一个名为"birdpedia"的新项目,并在github上以我的名字创建该项目的存储库,这样我的项目存储库的位置将在"github.com/dream2012/birdwiki"上,则该项目在我的计算机上的位置为:$GOPATH/src/github.com/dream2012/birdwiki

启动http服务器

现在几乎所有语言都实现了内置的服务器功能,golang亦是如此。

创建一个main.go

// This is the name of our package
// Everything with this package name can see everything
// else inside the same package, regardless of the file they are in
package main

// These are the libraries we are going to use
// Both "fmt" and "net" are part of the Go standard library
import (
    // "fmt" has methods for formatted I/O operations (like printing to the console)
    "fmt"
    // The "net/http" library has methods to implement HTTP clients and servers
    "net/http"
)

func main() {
    // The "HandleFunc" method accepts a path and a function as arguments
    // (Yes, we can pass functions as arguments, and even trat them like variables in Go)
    // However, the handler function has to have the appropriate signature (as described by the "handler" function below)
    http.HandleFunc("/", handler)

    // After defining our server, we finally "listen and serve" on port 8080
    // The second argument is the handler, which we will come to later on, but for now it is left as nil,
    // and the handler defined above (in "HandleFunc") is used
    http.ListenAndServe(":8080", nil)
}

// "handler" is our handler function. It has to follow the function signature of a ResponseWriter and Request type
// as the arguments.
func handler(w http.ResponseWriter, r *http.Request) {
    // For this case, we will always pipe "Hello World" into the response writer
    fmt.Fprintf(w, "Hello World!")
}
go run main.go

我们打开浏览器,访问http://127.0.0.1:8080/ 就可以看见"Hello Wolrd!"

设置路由

现在我们无论如何修改url,看到的都是hello world,这是因为我们没有设置路由,就是根据不同url进行不同的操作处理。

安装mux路由

go get github.com/gorilla/mux
package main

import (
    // Import the gorilla/mux library we just installed
    "fmt"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    // Declare a new router
    r := mux.NewRouter()

    // This is where the router is useful, it allows us to declare methods that
    // this path will be valid for
    r.HandleFunc("/hello", handler).Methods("GET")

    // We can then pass our router (after declaring all our routes) to this method
    // (where previously, we were leaving the secodn argument as nil)
    http.ListenAndServe(":8080", r)
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
}

这回我们只有访问http://127.0.0.1/hello 才能看到hello world,其它地址会看到404

测试

好的项目是需要有测试的,这里我们新建一个测试文件1_test.go
这里命名规则采取*_test.go
当我们执行go test 的时候,它会自动搜索_test.go文件进行测试

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHandler(t *testing.T) {
    //Here, we form a new HTTP request. This is the request that's going to be
    // passed to our handler.
    // The first argument is the method, the second argument is the route (which
    //we leave blank for now, and will get back to soon), and the third is the
    //request body, which we don't have in this case.
    req, err := http.NewRequest("GET", "", nil)

    // In case there is an error in forming the request, we fail and stop the test
    if err != nil {
        t.Fatal(err)
    }

    // We use Go's httptest library to create an http recorder. This recorder
    // will act as the target of our http request
    // (you can think of it as a mini-browser, which will accept the result of
    // the http request that we make)
    recorder := httptest.NewRecorder()

    // Create an HTTP handler from our handler function. "handler" is the handler
    // function defined in our main.go file that we want to test
    hf := http.HandlerFunc(handler)

    // Serve the HTTP request to our recorder. This is the line that actually
    // executes our the handler that we want to test
    hf.ServeHTTP(recorder, req)

    // Check the status code is what we expect.
    if status := recorder.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }

    // Check the response body is what we expect.
    expected := `Hello World!`
    actual := recorder.Body.String()
    if actual != expected {
        t.Errorf("handler returned unexpected body: got %v want %v", actual, expected)
    }
}

静态文件

“静态文件”是构成网站所需的HTML,CSS,JavaScript,图像和其他静态资产文件。

为了使我们的服务器为这些静态资产提供服务,我们需要执行3个步骤。

  • 创建静态文件
  • 修改我们的路由器以提供静态文件
  • 添加测试以验证我们的新服务器可以提供静态文件

创建static文件夹,创建静态文件index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>staic html file</h1>
</body>
</html>
// This is the name of our package
// Everything with this package name can see everything
// else inside the same package, regardless of the file they are in
package main

// These are the libraries we are going to use
// Both "fmt" and "net" are part of the Go standard library
import (
    // "fmt" has methods for formatted I/O operations (like printing to the console)
    "fmt"
    // The "net/http" library has methods to implement HTTP clients and servers
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    // import router

    r := mux.NewRouter()
    r.HandleFunc("/hello", handler).Methods("GET")

    staticFileDirectory := http.Dir("./static/")

    staticFileHandler := http.StripPrefix("/static/", http.FileServer(staticFileDirectory))
    // The "PathPrefix" method acts as a matcher, and matches all routes starting
    // with "/assets/", instead of the absolute route itself
    r.PathPrefix("/static/").Handler(staticFileHandler).Methods("GET")

    // The "HandleFunc" method accepts a path and a function as arguments
    // (Yes, we can pass functions as arguments, and even trat them like variables in Go)
    // However, the handler function has to have the appropriate signature (as described by the "handler" function below)
    //http.HandleFunc("/", handler)

    // After defining our server, we finally "listen and serve" on port 8080
    // The second argument is the handler, which we will come to later on, but for now it is left as nil,
    // and the handler defined above (in "HandleFunc") is used
    http.ListenAndServe(":8080", r)
}

// "handler" is our handler function. It has to follow the function signature of a ResponseWriter and Request type
// as the arguments.
func handler(w http.ResponseWriter, r *http.Request) {
    // For this case, we will always pipe "Hello World" into the response writer
    fmt.Fprintf(w, "Hello World!")
}
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gorilla/mux"
)

func newRouter() *mux.Router {
    r := mux.NewRouter()
    r.HandleFunc("/hello", handler).Methods("GET")

    // Declare the static file directory and point it to the
    // directory we just made
    staticFileDirectory := http.Dir("./static/")

    staticFileHandler := http.StripPrefix("/static/", http.FileServer(staticFileDirectory))

    r.PathPrefix("/static/").Handler(staticFileHandler).Methods("GET")
    return r
}

func TestStaticFileServer(t *testing.T) {
    r := newRouter()
    mockServer := httptest.NewServer(r)

    // We want to hit the `GET /assets/` route to get the index.html file response
    resp, err := http.Get(mockServer.URL + "/static/")
    if err != nil {
        t.Fatal(err)
    }

    // We want our status to be 200 (ok)
    if resp.StatusCode != http.StatusOK {
        t.Errorf("Status should be 200, got %d", resp.StatusCode)
    }

    // It isn't wise to test the entire content of the HTML file.
    // Instead, we test that the content-type header is "text/html; charset=utf-8"
    // so that we know that an html file has been served
    contentType := resp.Header.Get("Content-Type")
    expectedContentType := "text/html; charset=utf-8"

    if expectedContentType != contentType {
        t.Errorf("Wrong content type, expected %s, got %s", expectedContentType, contentType)
    }

}

创建表单 声明对象

golang中我们使用最后标识变量类型例如

type Bird struct{
    Species     string `json:"species"`
    Description string `json:"description"`
}
<!DOCTYPE html>
<html lang="en">

<head>
 <title>The bird encyclopedia</title>
</head>

<body>
  <h1>The bird encyclopedia</h1>
  <!--
    This section of the document specifies the table that will
    be used to display the list of birds and their description
   -->
  <table>
    <tr>
      <th>Species</th>
      <th>Description</th>
    </tr>
    <td>Pigeon</td>
    <td>Common in cities</td>
    </tr>
  </table>
  <br/>

  <!--
    This section contains the form, that will be used to hit the
    `POST /bird` API that we will build in the next section
   -->
  <form action="/bird" method="post">
    Species:
    <input type="text" name="species">
    <br/> Description:
    <input type="text" name="description">
    <br/>
    <input type="submit" value="Submit">
  </form>

  <!--
    Finally, the last section is the script that will
    run on each page load to fetch the list of birds
    and add them to our existing table
   -->
  <script>
    birdTable = document.querySelector("table")

    /*
    Use the browsers `fetch` API to make a GET call to /bird
    We expect the response to be a JSON list of birds, of the
    form :
    [
      {"species":"...","description":"..."},
      {"species":"...","description":"..."}
    ]
    */
    fetch("/bird")
      .then(response => response.json())
      .then(birdList => {
        //Once we fetch the list, we iterate over it
        birdList.forEach(bird => {
          // Create the table row
          row = document.createElement("tr")

          // Create the table data elements for the species and
                    // description columns
          species = document.createElement("td")
          species.innerHTML = bird.species
          description = document.createElement("td")
          description.innerHTML = bird.description

          // Add the data elements to the row
          row.appendChild(species)
          row.appendChild(description)
          // Finally, add the row element to the table itself
          birdTable.appendChild(row)
        })
      })
  </script>
</body>
</html>

处理程序

// This is the name of our package
// Everything with this package name can see everything
// else inside the same package, regardless of the file they are in
package main

// These are the libraries we are going to use
// Both "fmt" and "net" are part of the Go standard library
import (
    // "fmt" has methods for formatted I/O operations (like printing to the console)
    "encoding/json"
    "fmt"
    // The "net/http" library has methods to implement HTTP clients and servers
    "net/http"

    "github.com/gorilla/mux"
)

type Bird struct {
    Species     string `json:"species"`
    Description string `json:"description"`
}

var birds []Bird

func getBirdHandler(w http.ResponseWriter, r *http.Request) {
    //Convert the "birds" variable to json
    birdListBytes, err := json.Marshal(birds)

    // If there is an error, print it to the console, and return a server
    // error response to the user
    if err != nil {
        fmt.Println(fmt.Errorf("Error: %v", err))
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    // If all goes well, write the JSON list of birds to the response
    w.Write(birdListBytes)
}

func createBirdHandler(w http.ResponseWriter, r *http.Request) {
    // Create a new instance of Bird
    bird := Bird{}

    // We send all our data as HTML form data
    // the `ParseForm` method of the request, parses the
    // form values
    err := r.ParseForm()

    // In case of any error, we respond with an error to the user
    if err != nil {
        fmt.Println(fmt.Errorf("Error: %v", err))
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    // Get the information about the bird from the form info
    bird.Species = r.Form.Get("species")
    bird.Description = r.Form.Get("description")

    // Append our existing list of birds with a new entry
    birds = append(birds, bird)

    //Finally, we redirect the user to the original HTMl page
    // (located at `/assets/`), using the http libraries `Redirect` method
    http.Redirect(w, r, "/assets/", http.StatusFound)
}

func main() {
    // import router

    r := mux.NewRouter()
    r.HandleFunc("/hello", handler).Methods("GET")

    staticFileDirectory := http.Dir("./static/")

    staticFileHandler := http.StripPrefix("/static/", http.FileServer(staticFileDirectory))
    // The "PathPrefix" method acts as a matcher, and matches all routes starting
    // with "/assets/", instead of the absolute route itself
    r.PathPrefix("/static/").Handler(staticFileHandler).Methods("GET")

    r.HandleFunc("/bird", getBirdHandler).Methods("GET")
    r.HandleFunc("/bird", createBirdHandler).Methods("POST")

    // The "HandleFunc" method accepts a path and a function as arguments
    // (Yes, we can pass functions as arguments, and even trat them like variables in Go)
    // However, the handler function has to have the appropriate signature (as described by the "handler" function below)
    //http.HandleFunc("/", handler)

    // After defining our server, we finally "listen and serve" on port 8080
    // The second argument is the handler, which we will come to later on, but for now it is left as nil,
    // and the handler defined above (in "HandleFunc") is used
    http.ListenAndServe(":8080", r)
}

// "handler" is our handler function. It has to follow the function signature of a ResponseWriter and Request type
// as the arguments.
func handler(w http.ResponseWriter, r *http.Request) {
    // For this case, we will always pipe "Hello World" into the response writer
    fmt.Fprintf(w, "Hello World!")
}

到此我们创建了一个简单wiki,我们可以自己添加数据,展示数据,剩下的就是如何保存数据到数据库了额,这个我们下回再说。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!