从0开始创建一个go web项目
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,我们可以自己添加数据,展示数据,剩下的就是如何保存数据到数据库了额,这个我们下回再说。