# lets-go **Repository Path**: lavenliu/lets-go ## Basic Information - **Project Name**: lets-go - **Description**: 一个 Golang WEB 站点。点击链接即可查看详情:https://snippetbox.lavenliu.cn/ - **Primary Language**: Go - **License**: AGPL-3.0 - **Default Branch**: main - **Homepage**: https://snippetbox.lavenliu.cn/ - **GVP Project**: No ## Statistics - **Stars**: 7 - **Forks**: 1 - **Created**: 2020-05-30 - **Last Updated**: 2025-06-17 ## Categories & Tags **Categories**: ebooks-manual **Tags**: None ## README # 从零开始使用 Golang 时至今日,该工程已经有了四年时间了。小白在这两年的时间里断断续续地完成了这个小 Demo。目前使用的 `Golang` 版本为最新版 `1.20`。 ```bash [root@developing ~]# go version go version go1.20.10 linux/amd64 ``` ## 初始化工程 ```bash cd $HOME/code/snippetbox go mod init lavenliu.cn/snippetbox ``` ## DB ```sql -- create a new utf8 `snippetbox` database create database snippetbox character set utf8 collate utf8_general_ci; -- Create a `snippets` table. CREATE TABLE snippets ( id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, content TEXT NOT NULL, created DATETIME NOT NULL, expires DATETIME NOT NULL ); -- Add an index on the created column. CREATE INDEX idx_snippets_created ON snippets(created); -- Add some dummy records (which we'll use in the next couple of chapters). INSERT INTO snippets (title, content, created, expires) VALUES ( 'An old silent pond', 'An old silent pond...\nA frog jumps into the pond,\nsplash! Silence again.\n\n– Matsuo Bashō', UTC_TIMESTAMP(), DATE_ADD(UTC_TIMESTAMP(), INTERVAL 365 DAY) ); INSERT INTO snippets (title, content, created, expires) VALUES ( 'Over the wintry forest', 'Over the wintry\nforest, winds howl in rage\nwith no leaves to blow.\n\n– Natsume Soseki', UTC_TIMESTAMP(), DATE_ADD(UTC_TIMESTAMP(), INTERVAL 365 DAY) ); INSERT INTO snippets (title, content, created, expires) VALUES ( 'First autumn morning', 'First autumn morning\nthe mirror I stare into\nshows my father''s face.\n\n– Murakami Kijo', UTC_TIMESTAMP(), DATE_ADD(UTC_TIMESTAMP(), INTERVAL 7 DAY) ); CREATE USER 'web'@'localhost'; GRANT SELECT, INSERT, UPDATE ON snippetbox.* TO 'web'@'localhost'; ALTER USER 'web'@'localhost' IDENTIFIED BY 'pass'; -- 测试数据库 mysql -D snippetbox -u web -p Enter password: mysql> SELECT id, title, expires FROM snippets; +----+------------------------+---------------------+ | id | title | expires | +----+------------------------+---------------------+ | 1 | An old silent pond | 2020-05-23 08:53:27 | | 2 | Over the wintry forest | 2020-05-23 08:53:27 | | 3 | First autumn morning | 2019-05-31 08:53:27 | | 4 | Laven Liu | 2019-05-31 09:48:06 | | 5 | Laven Liu | 2019-05-31 09:49:30 | | 6 | Laven Liu | 2019-05-31 09:49:31 | +----+------------------------+---------------------+ 6 rows in set (0.01 sec) -- 创建用户表 USE snippetbox; CREATE TABLE users ( id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, hashed_password CHAR(60) NOT NULL, created DATETIME NOT NULL, active BOOLEAN NOT NULL DEFAULT TRUE ); ALTER TABLE users ADD CONSTRAINT users_uc_email UNIQUE (email); ``` ## 安装数据库驱动 ```bash $ cd $HOME/code/snippetbox $ go get github.com/go-sql-driver/mysql@v1 go: finding github.com/go-sql-driver/mysql v1.4.1 go: downloading github.com/go-sql-driver/mysql v1.4.1 $ cat go.mod module lavenliu.cn/snippetbox require github.com/go-sql-driver/mysql v1.4.1 $ cat go.sum github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= ``` ## 连接数据库 ```go db, err := sql.Open("mysql", "web:pass@/snippetbox?parseTime=true") if err != nil { ... } ``` Behind the scenes of rows.Scan() your driver will automatically convert the raw output from the SQL database to the required native Go types. So long as you’re sensible with the types that you’re mapping between SQL and Go, these conversions should generally Just Work. Usually: - CHAR, VARCHAR and TEXT map to string. - BOOLEAN maps to bool. - INT maps to int; BIGINT maps to int64. - DECIMAL and NUMERIC map to float. - TIME, DATE and TIMESTAMP map to time.Time. - The `parseTime=true` part of the DSN above is a driver-specific parameter which instructs our driver to convert SQL `TIME` and `DATE` fields to Go `time.Time` objects.(指示我们的驱动,把 SQL 的 `TIME` 及 `DATE` 转换成对应 Go 语言的 `time.Time` 对象) - The `sql.Open()` function returns a sql.DB object. This isn’t a database connection — **it’s a pool of many connections**. This is an important difference to understand. Go manages these connections as needed, automatically opening and closing connections to the database via the driver. ## 执行SQL语句 ```sql INSERT INTO snippets (title, content, created, expires) VALUES(?, ?, UTC_TIMESTAMP(), DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? DAY)) ``` Go provides three different methods for executing database queries: - `DB.Query()` is used for `SELECT` queries which return multiple rows. - `DB.QueryRow()` is used for `SELECT` queries which return a single row. - `DB.Exec()` is used for statements which don’t return rows (like `INSERT` and `DELETE`). ## 额外信息 ### Module Paths for Downloadable Packages If we are creating a package or application which can be downloaded and used by other people and programs, then it’s good practice for your module path to equal the location that the code can be downloaded from. For instance, if our package is hosted at `https://github.com/foo/bar` then the module path for the project should be `github.com/foo/bar`. ### Web Application Basics We’ll begin with the three absolute essentials: - The first thing we need is a handler. If you’re coming from an MVC-background, you can think of handlers as being a bit like controllers. They’re responsible for executing your application logic and for writing HTTP response headers and bodies. - The second component is a router (or servemux in Go terminology). This stores a mapping between the URL patterns for your application and the corresponding handlers. Usually you have one servemux for your application containing all your routes. - The last thing we need is a web server. One of the great things about Go is that you can establish a web server and listen for incoming requests *as part of your application itself*. You don’t need an external third-party server like Nginx or Apache. A simplest http server code, ```go package main import ( "log" "net/http" ) // Define a home handler function which writes a byte slice containing func home(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello World!")) } func main() { // Use the http.NewServeMux() function to initialize a new servemux, then // register the home function as the handler for the "/" URL pattern. mux := http.NewServeMux() mux.HandleFunc("/", home) // Use the http.ListenAndServe() function to start a new web server. We pass in // two parameters: the TCP network address to listen on (in this case ":4000") // and the servemux we just created. If http.ListenAndServe() returns an error // we use the log.Fatal() function to log the error message and exit. Note // that any error returned by http.ListenAndServe() is always non-nil. log.Println("Starting server on :4000") err := http.ListenAndServe(":4000", mux) log.Fatal(err) } ``` > **Note:** The `home` handler function is just a regular Go function with two parameters. The `http.ResponseWriter` parameter provides methods for assembling a HTTP response and sending it to the user, and the `*http.Request` parameter is a pointer to a struct which holds information about the current request (like the HTTP method and the URL being requested). Run the code, and use `curl` tool to test it. ```bash go run main.go ``` > **Important:** Before we continue, I should explain that Go’s servemux treats the URL pattern `"/"` like a catch-all. So at the moment *all* HTTP requests to our server will be handled by the `home` function, regardless of their URL path. For instance, you can visit a different URL path like [`http://localhost:4000/foo`](http://localhost:4000/foo) and you’ll receive exactly the same response. ### Network Addresses The TCP network address that you pass to `http.ListenAndServe()` should be in the format `"host:port"`. If you omit the host (like we did with `":4000"`) then the server will listen on all your computer’s available network interfaces. Generally, you only need to specify a host in the address if your computer has multiple network interfaces and you want to listen on just one of them. In other Go projects or documentation you might sometimes see network addresses written using named ports like `":http"` or `":http-alt"` instead of a number. If you use a named port then Go will attempt to look up the relevant port number from your `/etc/services` file when starting the server, or will return an error if a match can’t be found. ### What About RESTful Routing? It’s important to acknowledge that the routing functionality provided by Go’s servemux is pretty lightweight. It doesn’t support routing based on the request method, it doesn’t support semantic URLs with variables in them, and it doesn’t support regexp-based patterns. If you have a background in using frameworks like Rails, Django or Laravel you might find this a bit restrictive… and surprising! ### Manipulating the Header Map In the code above we used `w.Header().Set()` to add a new header to the response header map. But there’s also `Add()`, `Del()` and `Get()` methods that you can use to read and manipulate the header map too. ```go // Set a new cache-control header. If an existing "Cache-Control" header exists // it will be overwritten. w.Header().Set("Cache-Control", "public, max-age=31536000") // In contrast, the Add() method appends a new "Cache-Control" header and can // be called multiple times. w.Header().Add("Cache-Control", "public") w.Header().Add("Cache-Control", "max-age=31536000") // Delete all values for the "Cache-Control" header. w.Header().Del("Cache-Control") // Retrieve the first value for the "Cache-Control" header. w.Header().Get("Cache-Control") ``` #### System-Generated Headers and Content Sniffing When sending a response Go will automatically set three system-generated headers for you: `Date` and `Content-Length` and `Content-Type`. The `Content-Type` header is particularly interesting. Go will attempt to set the correct one for you by content sniffing the response body with the [`http.DetectContentType()`](https://golang.org/pkg/net/http/#DetectContentType) function. If this function can’t guess the content type, Go will fall back to setting the header `Content-Type: application/octet-stream` instead. The `http.DetectContentType()` function generally works quite well, but a common gotcha for web developers new to Go is that it can’t distinguish JSON from plain text. So, by default, JSON responses will be sent with a `Content-Type: text/plain; charset=utf-8` header. You can prevent this from happening by setting the correct header manually like so: ```go w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"name":"Alex"}`)) ``` #### Header Canonicalization When you’re using the `Add()`, `Get()`, `Set()` and `Del()` methods on the header map, the header name will always be canonicalized using the [`textproto.CanonicalMIMEHeaderKey()`](https://golang.org/pkg/net/textproto/#CanonicalMIMEHeaderKey) function. This converts the first letter and any letter following a hyphen to upper case, and the rest of the letters to lowercase. This has the practical implication that when calling these methods the header name is *case-insensitive*. If you need to avoid this canonicalization behavior you can edit the underlying header map directly (it has the type `map[string][]string`). For example: ```go w.Header()["X-XSS-Protection"] = []string{"1; mode=block"} ``` **Note:** If a HTTP/2 connection is being used, Go will *always* automatically convert the header names and values to lowercase for you as per [the HTTP/2 specifications](https://tools.ietf.org/html/rfc7540#section-8.1.2). #### Suppressing System-Generated Headers The `Del()` method doesn’t remove system-generated headers. To suppress these, you need to access the underlying header map directly and set the value to `nil`. If you want to suppress the `Date` header, for example, you need to write: ```go w.Header()["Date"] = nil ``` ### Requests Are Handled Concurrently There is one more thing that’s really important to point out: *all incoming HTTP requests are served in their own goroutine*. For busy servers, this means it’s very likely that the code in or called by your handlers will be running concurrently. While this helps make Go blazingly fast, the downside is that you need to be aware of (and protect against) [race conditions](https://www.alexedwards.net/blog/understanding-mutexes) when accessing shared resources from your handlers. ### Go package management #### Upgrading Packages Once a package has been downloaded and added to your `go.mod` file the package and version are ‘fixed’. But there’s many reasons why you might want to upgrade to use a newer version of a package in the future. To upgrade to latest available *minor or patch release* of a package, you can simply run `go get` with the `-u` flag like so: ```bash $ go get -u github.com/foo/bar ``` Or alternatively, if you want to upgrade to a specific version then you should run the same command but with the appropriate `@version` suffix. For example: ```bash $ go get -u github.com/foo/bar@v2.0.0 ``` #### Removing Unused Packages Sometimes you might `go get` a package only to realize later in your build that you don’t need it anymore. When this happens you’ve got two choices. You could either run `go get` and postfix the package path with `@none`, like so: ```bash $ go get github.com/foo/bar@none ``` Or if you’ve removed all references to the package in your code, you could run `go mod tidy`, which will automatically remove any unused packages from your `go.mod` and `go.sum` files. ```bash $ go mod tidy -v ``` ### `r.Form` map In our code above, we accessed the form values via the `r.PostForm` map. But an alternative approach is to use the (subtly different) `r.Form` map. The `r.PostForm` map is populated only for `POST`, `PATCH` and `PUT` requests, and contains the form data from the request body. In contrast, the `r.Form` map is populated for all requests (irrespective of their HTTP method), and contains the form data from any request body **and** any query string parameters. So, if our form was submitted to `/snippet/create?foo=bar`, we could also get the value of the `foo` parameter by calling `r.Form.Get("foo")`. Note that in the event of a conflict, the request body value will take precedent over the query string parameter. Using the `r.Form` map can be useful if your application sends data in a HTML form and in the URL, or you have an application that is agnostic about how parameters are passed. But in our case those things aren’t applicable. We expect our form data to be sent in the request body only, so it’s for sensible for us to access it via `r.PostForm`. ### 创建自签名 TLS 证书 ```bash $ cd $HOME/code/snippetbox $ mkdir tls $ cd tls ``` To run the `generate_cert.go` tool, you’ll need to know the place on your computer where the source code for the Go standard library is installed. If you’re using Linux, macOS or FreeBSD and followed the [official install instructions](https://golang.org/doc/install#install), then the `generate_cert.go` file should be located under `/usr/local/go/src/crypto/tls`. If you’re using macOS and installed Go using Homebrew, the file will probably be at `/usr/local/Cellar/go//libexec/src/crypto/tls/generate_cert.go` or a similar path. Once you know where it is located, you can then run the `generate_cert.go` tool like so: ```bash ➜ lets-go-gitee git:(dev) ✗ ls /usr/local/go/src/crypto/tls alert.go handshake_server.go auth.go handshake_server_test.go auth_test.go handshake_server_tls13.go cipher_suites.go handshake_test.go common.go handshake_unix_test.go common_string.go key_agreement.go conn.go key_schedule.go conn_test.go key_schedule_test.go example_test.go link_test.go generate_cert.go prf.go handshake_client.go prf_test.go handshake_client_test.go testdata handshake_client_tls13.go ticket.go handshake_messages.go tls.go handshake_messages_test.go tls_test.go ➜ tls git:(dev) ✗ go run /usr/local/go/src/crypto/tls/generate_cert.go --rsa-bits=2048 --host=localhost 2021/05/11 12:32:48 wrote cert.pem 2021/05/11 12:32:48 wrote key.pem ``` Behind the scenes the `generate_cert.go` tool works in two stages: 1. First it generates a 2048-bit RSA key pair, which is a cryptographically secure [public key and private key](https://en.wikipedia.org/wiki/Public-key_cryptography). 2. It then stores the private key in a `key.pem` file, and generates a self-signed TLS certificate for the host `localhost` containing the public key — which it stores in a `cert.pem` file. Both the private key and certificate are PEM encoded, which is the standard format used by most TLS implementations. ### HTTP Requests It’s important to note that our HTTPS server *only supports HTTPS*. If you try making a regular HTTP request to it, it won’t work. But exactly what happens depends on the version of Go that you’re running. In Go version 1.12 and newer, the server will send the user a `400 Bad Request` status and the message `"Client sent an HTTP request to an HTTPS server"`. Nothing will be logged. In older versions of Go, the server will write the bytes `15 03 01 00 02 02 0A` to the underlying TCP connection, which essentially is TLS-speak for “I don’t understand”, and you’ll also see a corresponding log message in your terminal similar to this: ```bash ➜ lets-go-gitee git:(dev) ✗ curl http://localhost:4000/ Client sent an HTTP request to an HTTPS server. # 日志输出为 ERROR 2021/05/11 12:43:32 server.go:3137: http: TLS handshake error from [::1]:52829: EOF ``` #### HTTP/2 Connections A big plus of using HTTPS is that — if a client supports [HTTP/2](https://en.wikipedia.org/wiki/HTTP/2) connections — Go’s HTTPS server will automatically upgrade the connection to use HTTP/2. This is good because it means that, ultimately, our pages will load faster for users. If you’re not familiar with HTTP/2 you can get a run-down of the basics and a flavor of how has been implemented behind the scenes in this [GoSF meetup talk](https://www.youtube.com/watch?v=FARQMJndUn0) by Brad Fitzpatrick. If you’re using an up-to-date version of Firefox you should be able to see this in action. Press `Ctrl+Shift+E` to open the Developer Tools, and if you look at the headers for the homepage you should see that the protocol being used is HTTP/2. ```bash ➜ lets-go-gitee git:(dev) ✗ curl -k -I https://localhost:4000 HTTP/2 200 x-frame-options: deny x-xss-protection: 1; mode=block content-type: text/html; charset=utf-8 content-length: 3102 date: Tue, 11 May 2021 04:45:39 GMT ``` ### How Request Context Works Every `http.Request` that our handlers process has a [`context.Context`](https://golang.org/pkg/context/#Context) object embedded in it, which we can use to store information during the lifetime of the request. The basic code for adding information to a request's context looks like this: ```go // Where r is a *http.Request... ctx := r.Context() ctx = context.WithValue(ctx, "isAuthenticated", true) r = r.WithContext(ctx) ``` Let’s step through this line-by-line. - First, we use the `r.Context()` method to retrieve the *existing* context from a request and assign it to the `ctx` variable. - Then we use the `context.WithValue()` method to create a *new copy* of the existing context, containing the key `"isAuthenticated"` and a value of `true`. - Then finally we use the `r.WithContext()` method to create a *copy* of the request containing our new context. > **Important:** Note that we don’t actually update the context for a request directly. What we’re doing is *creating a new copy* of the `http.Request` object with our new context in it. I should also point out that, for clarity, I made that code snippet a bit more verbose than it needs to be. It’s typical to shorten it a little bit like so: ```go ctx = context.WithValue(r.Context(), "isAuthenticated", true) r = r.WithContext(ctx) ``` The important thing to explain here is that, behind the scenes, request context values are stored with the type `interface{}`. And that means that, after retrieving them from the context, you’ll need to assert them to their original type before you use them. To retrieve a value we need to use the `r.Context().Value()` method, like so: ```go isAuthenticated, ok := r.Context().Value("isAuthenticated").(bool) if !ok { return errors.New("could not convert value to bool") } ``` In the code samples above, I’ve used the string `"isAuthenticated"` as the key for storing and retrieving the data from a request context. But this isn’t recommended, because there’s a risk that other third-party packages used by your application will also want to store data using the key `"isAuthenticated"`. And that would cause a naming collision and bugginess. To avoid this, it’s good practice to create your own custom type which you can use for your context keys. Extending our sample code, it’s much better to do something like this: ```go type contextKey string const contextKeyIsAuthenticated = contextKey("isAuthenticated") ... ctx := r.Context() ctx = context.WithValue(ctx, contextKeyIsAuthenticated, true) r = r.WithContext(ctx) ... isAuthenticated, ok := r.Context().Value(contextKeyIsAuthenticated).(bool) if !ok { return errors.New("could not convert value to bool") } ``` ### 测试 In Go, its standard practice to create your tests in `*_test.go` files which live directly alongside code that you’re testing. ```go package main import ( "testing" "time" ) func TestHumanDate(t *testing.T) { // Initialize a new time.Time object and pass it to the humanDate function. tm := time.Date(2020, 12, 17, 10, 0, 0, 0, time.UTC) hd := humanDate(tm) // Check that the output from the humanDate function is in the format we // expect. If it isn't what we expect, use the t.Errorf() function to // indicate that the test has failed and log the expected and actual // values. if hd != "17 Dec 2020 at 10:00" { t.Errorf("want %q; got %q", "17 Dec 2020 at 10:00", hd) } } ``` This pattern is the basic one that you’ll use for nearly all tests that you write in Go. The important things to take away are: - Your unit tests are contained in a normal Go function with the signature `func(*testing.T)`. - To be a valid unit test the name of this function *must* begin with the word `Test`. Typically this is then followed by the name of the function, method or type that you’re testing to help make it obvious at a glance *what* is being tested. - You can use the [`t.Errorf()`](https://golang.org/pkg/testing/#T.Errorf) function to mark a test as *failed* and log a descriptive message about the failure. Let’s try this out. Save the file, then use the `go test` command to run all the tests in our `cmd/web` package like so: ```bash $ go test ./cmd/web ok lavenliu.cn/snippetbox/cmd/web 0.005s ``` So, this is good stuff. The `ok` in this output indicates that all tests in the package (for now, only our `TestHumanDate()` test) passed without any problems. If you want more detail, you can see exactly which tests are being run by using the `-v` flag to get the *verbose* output: ```bash $ go test -v ./cmd/web === RUN TestHumanDate --- PASS: TestHumanDate (0.00s) PASS ok lavenliu.cn/snippetbox/cmd/web 0.007s ``` #### Table-Driven Tests Let’s now expand our `TestHumanDate()` function to cover some additional test cases. Specifically, we’re going to update it to also check that: 1. If the input to `humanDate()` is the [zero time](https://golang.org/pkg/time/#Time.IsZero), then it returns the empty string `""`. 2. The output from the `humanDate()` function always uses the UTC time zone. In Go, an idiomatic way to run multiple test cases is to use table-driven tests. Essentially, the idea behind table-driven tests is to create a *table* of test cases containing the inputs and expected outputs, and to then loop over these, running each test case in a sub-test. There are a few ways you could set this up, but a common approach is to define your test cases in an slice of anonymous structs. > Use the `t.Run()` function to run a sub-test for each test case. The first parameter to this is the name of the test (which is used to identify the sub-test in any log output) and the second parameter is and anonymous function containing the actual test for each case. ```go package main import ( "testing" "time" ) func TestHumanDate(t *testing.T) { // Create a slice of anonymous structs containing the test case name, // input to our humanDate() function (the tm field), and expected output // (the want field). tests := []struct { name string tm time.Time want string }{ { name: "UTC", tm: time.Date(2020, 12, 17, 10, 0, 0, 0, time.UTC), want: "17 Dec 2020 at 10:00", }, { name: "Empty", tm: time.Time{}, want: "", }, { name: "CET", tm: time.Date(2020, 12, 17, 10, 0, 0, 0, time.FixedZone("CET", 1*60*60)), want: "17 Dec 2020 at 09:00", }, } // Loop over the test cases. for _, tt := range tests { // Use the t.Run() function to run a sub-test for each test case. The // first parameter to this is the name of the test (which is used to // identify the sub-test in any log output) and the second parameter is // and anonymous function containing the actual test for each case. t.Run(tt.name, func(t *testing.T) { hd := humanDate(tt.tm) if hd != tt.want { t.Errorf("want %q; got %q", tt.want, hd) } }) } } ``` > **Note:** In the third test case we’re using CET (Central European Time) as the time zone, which is one hour ahead of UTC. So we want the output from `humanDate()` (in UTC) to be `17 Dec 2020 at 09:00`, not `17 Dec 2020 at 10:00`. OK, let’s run this and see what happens: ```bash $ go test -v ./cmd/web === RUN TestHumanDate === RUN TestHumanDate/UTC === RUN TestHumanDate/Empty === RUN TestHumanDate/CET --- FAIL: TestHumanDate (0.00s) --- PASS: TestHumanDate/UTC (0.00s) --- FAIL: TestHumanDate/Empty (0.00s) templates_test.go:44: want ""; got "01 Jan 0001 at 00:00" --- FAIL: TestHumanDate/CET (0.00s) templates_test.go:44: want "17 Dec 2020 at 09:00"; got "17 Dec 2020 at 10:00" FAIL FAIL lavenliu.cn/snippetbox/cmd/web 0.005s ``` We can see that we get individual output for each of our sub-tests. As you might have guessed, our first test case passed but the `Empty` and `CET` tests both failed. Notice how — for the failed test cases — we get the relevant failure message and filename and line number in the output? It also worth pointing out that when we use the `t.Errorf()` function to mark a test as failed, it doesn’t cause `go test` to immediately exit. All the other tests and sub-tests will continue to be run after a failure. As a side note, you can use the `-failfast` flag to stop the tests running after the first failure, if you want, like so: ```bash $ go test -failfast -v ./cmd/web === RUN TestHumanDate === RUN TestHumanDate/UTC === RUN TestHumanDate/Empty --- FAIL: TestHumanDate (0.00s) --- PASS: TestHumanDate/UTC (0.00s) --- FAIL: TestHumanDate/Empty (0.00s) templates_test.go:44: want ""; got "01 Jan 0001 at 00:00" FAIL FAIL lavenliu.cn/snippetbox/cmd/web 0.007s ``` #### Running All Tests To run *all* the tests for a project — instead of just those in a specific package — you can use the `./...` wildcard pattern like so: ```bash $ go test ./... ok lavenliu.cn/snippetbox/cmd/web 0.006s ? lavenliu.cn/snippetbox/pkg/forms [no test files] ? lavenliu.cn/snippetbox/pkg/models [no test files] ? lavenliu.cn/snippetbox/pkg/models/mysql [no test files] ``` #### Running Specific Tests It’s possible to only run specific tests by using the `-run` flag. This allows you to pass in a regular expression — and only tests with a name that matches the regular expression will be run. In our case, we could opt to run only the `TestPing` test like so: ```bash $ go test -v -run="^TestPing$" ./cmd/web/ === RUN TestPing --- PASS: TestPing (0.01s) PASS ok lavenliu.cn/snippetbox/cmd/web 0.012s ``` And you can even use the `-run` flag to limit testing to some specific sub-tests. For example: ```bash $ go test -v -run="^TestHumanDate$/^UTC|CET$" ./cmd/web === RUN TestHumanDate === RUN TestHumanDate/UTC === RUN TestHumanDate/CET --- PASS: TestHumanDate (0.00s) --- PASS: TestHumanDate/UTC (0.00s) --- PASS: TestHumanDate/CET (0.00s) PASS ok lavenliu.cn/snippetbox/cmd/web 0.007s ``` Note how, when it comes to running specific sub-tests, the value of the `-run` flag contains multiple regular expressions separated by a `/` character? The first part needs to match the name of the test, and the second part needs to match the name of the sub-test. #### Parallel Testing By default, the `go test` command executes all tests in a serial manner, one after another. When you have a small number of tests (like we do) and the runtime is very fast, this is absolutely fine. But if you have hundreds or thousands of tests the total run time can start adding up to something more meaningful. And in that scenario, you may save yourself some time by running your tests in parallel. You can indicate that it’s OK for a test to be run in concurrently alongside other tests by calling the `t.Parallel()` function at the start of the test. For example: ```go func TestPing(t *testing.T) { t.Parallel() ... } ``` It’s important to note here that: - Tests marked using `t.Parallel()` will be run in parallel with — *and only with* — other parallel tests. - By default, the maximum number of tests that will be run simultaneously is the current value of [GOMAXPROCS](https://golang.org/pkg/runtime/#pkg-constants). You can override this by setting a specific value via the `-parallel` flag. For example: ```bash $ go test -parallel 4 ./... ``` - Not all tests are suitable to be run in parallel. For example, if you have an integration test which requires a database table to be in a specific known state, then you wouldn’t want to run it in parallel with other tests that manipulate the same database table. #### Enabling the Race Detector The `go test` command includes a `-race` flag which enables Go’s [race detector](https://golang.org/doc/articles/race_detector.html) when running tests. If the code you’re testing leverages concurrency, or you’re running tests in parallel, enabling this can be a good idea to help to flag up race conditions that exist in your application. You can use it like so: ```bash $ go test -race ./cmd/web/ ``` It’s important to point out that the race detector is just a tool that flags data races if and when they occur at runtime. It doesn’t carry out static analysis of your codebase, and a clear run doesn’t *ensure* that your code is free of race conditions. Enabling the race detector will also increase the overall running time of your tests. So if you’re running tests very frequently part of a TDD workflow, you may prefer to use the `-race` flag during pre-commit test runs only. ## 运行项目 ```bash PS D:\home\code\snippetbox> go run ./cmd/web INFO 2019/11/21 17:40:12 Starting server on :4000 ``` 运行效果为: ![图1](images/01.png) ![图2](images/02.png) ![用户登录](images/user_signup_and_login.png) ![用户退出](images/user_logout.png) ## 部署项目 ```bash # /lib/systemd/system/goweb.service [Unit] Description=goweb [Service] WorkingDirectory=/data/apps Type=simple Restart=always RestartSec=5s ExecStart=/home/user/go/go-web/main [Install] WantedBy=multi-user.target ``` Nginx: ```nginx server { listen 80; server_name snippetbox.lavenliu.cn; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_pass http://127.0.0.1:4000; } location ~ /.well-known { allow all; } } ``` ## Swagger UI ```bash $ go get -u github.com/go-swagger/go-swagger/cmd/swagger $ swagger version version: v0.27.0 commit: (unknown, mod sum: "h1:K7+nkBuf4oS1jTBrdvWqYFpqD69V5CN8HamZzCDDhAI=") ```