时至今日,该工程已经有了四年时间了。小白在这两年的时间里断断续续地完成了这个小 Demo。目前使用的 Golang
版本为最新版 1.20
。
[root@developing ~]# go version
go version go1.20.10 linux/amd64
cd $HOME/code/snippetbox
go mod init lavenliu.cn/snippetbox
-- 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);
$ 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=
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:
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
对象)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.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
).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
.
We’ll begin with the three absolute essentials:
A simplest http server code,
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. Thehttp.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.
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 thehome
function, regardless of their URL path. For instance, you can visit a different URL path likehttp://localhost:4000/foo
and you’ll receive exactly the same response.
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.
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!
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.
// 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")
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()
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:
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"name":"Alex"}`))
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()
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:
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.
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:
w.Header()["Date"] = nil
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 when accessing shared resources from your handlers.
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:
$ 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:
$ go get -u github.com/foo/bar@v2.0.0
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:
$ 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.
$ go mod tidy -v
r.Form
mapIn 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
.
$ 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, 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/<version>/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:
➜ 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:
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.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:
➜ 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
A big plus of using HTTPS is that — if a client supports 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 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.
➜ 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
Every http.Request
that our handlers process has a 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:
// 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.
r.Context()
method to retrieve the existing context from a request and assign it to the ctx
variable.context.WithValue()
method to create a new copy of the existing context, containing the key "isAuthenticated"
and a value of true
.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:
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:
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:
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.
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:
func(*testing.T)
.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.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:
$ 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:
$ go test -v ./cmd/web
=== RUN TestHumanDate
--- PASS: TestHumanDate (0.00s)
PASS
ok lavenliu.cn/snippetbox/cmd/web 0.007s
Let’s now expand our TestHumanDate()
function to cover some additional test cases. Specifically, we’re going to update it to also check that:
humanDate()
is the zero time, then it returns the empty string ""
.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.
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 be17 Dec 2020 at 09:00
, not17 Dec 2020 at 10:00
.
OK, let’s run this and see what happens:
$ 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:
$ 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
To run all the tests for a project — instead of just those in a specific package — you can use the ./...
wildcard pattern like so:
$ 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]
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:
$ 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:
$ 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.
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:
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. You can override this by setting a specific value via the -parallel
flag. For example:
$ 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.
The go test
command includes a -race
flag which enables Go’s race detector 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:
$ 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.
PS D:\home\code\snippetbox> go run ./cmd/web
INFO 2019/11/21 17:40:12 Starting server on :4000
运行效果为:
# /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:
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;
}
}
$ go get -u github.com/go-swagger/go-swagger/cmd/swagger
$ swagger version
version: v0.27.0
commit: (unknown, mod sum: "h1:K7+nkBuf4oS1jTBrdvWqYFpqD69V5CN8HamZzCDDhAI=")
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。
1. 开源生态
2. 协作、人、软件
3. 评估模型