diff options
| author | Steve Traut <straut@google.com> | 2021-07-01 13:28:40 -0400 |
|---|---|---|
| committer | Steve Traut <straut@google.com> | 2021-07-12 15:00:05 +0000 |
| commit | a46b181d9b5223ead339fdc5c06809d25f970703 (patch) | |
| tree | 3f9845fb445a880fea3f43b5ce94c4018ad47c50 /_content/doc/tutorial | |
| parent | aee54c06b6e063ed170479ec8c4757b211a1276f (diff) | |
| download | go-x-website-a46b181d9b5223ead339fdc5c06809d25f970703.tar.xz | |
_content/doc: add a Go/Gin web service tutorial
This adds a tutorial introducing how to build a RESTful web service
using Go and the Gin web framework.
This change includes:
- A RESTful web service tutorial:
- _content/doc/tutorial/web-service-gin.md
- Updates to index topics so that they list the new content.
- _content/doc/index.html
- _content/doc/tutorial/index.html
Change-Id: I137dcd884c4e38b4a2bebd733ec630f2d9bc3b95
Reviewed-on: https://go-review.googlesource.com/c/website/+/332349
Trust: Steve Traut <straut@google.com>
Reviewed-by: Russ Cox <rsc@golang.org>
Diffstat (limited to '_content/doc/tutorial')
| -rw-r--r-- | _content/doc/tutorial/index.html | 4 | ||||
| -rw-r--r-- | _content/doc/tutorial/web-service-gin.md | 651 |
2 files changed, 653 insertions, 2 deletions
diff --git a/_content/doc/tutorial/index.html b/_content/doc/tutorial/index.html index 4746ae8a..31abf8b4 100644 --- a/_content/doc/tutorial/index.html +++ b/_content/doc/tutorial/index.html @@ -40,14 +40,14 @@ <td class="DocTable-cell">Introduces the basics of accessing a database using the standard library.</td> </tr> - <!-- <tr class="DocTable-row"> + <tr class="DocTable-row"> <td class="DocTable-cell"> <a href="/doc/tutorial/web-service-gin">Developing a RESTful API with Go and Gin</a> </td> <td class="DocTable-cell">Introduces the basics of writing a RESTful web service API with Go and the Gin Web Framework.</td> - </tr> --> + </tr> <tr class="DocTable-row"> <td class="DocTable-cell"> <a href="https://tour.golang.org/welcome/1">A Tour of Go</a> diff --git a/_content/doc/tutorial/web-service-gin.md b/_content/doc/tutorial/web-service-gin.md new file mode 100644 index 00000000..229bdb51 --- /dev/null +++ b/_content/doc/tutorial/web-service-gin.md @@ -0,0 +1,651 @@ +<!--{ + "Title": "Tutorial: Developing a RESTful API with Go and Gin" +}--> + +This tutorial introduces the basics of writing a RESTful web service API with Go +and the [Gin Web Framework](https://gin-gonic.com/docs/) (Gin). + +You'll get the most out of this tutorial if you have a basic familiarity with Go +and its tooling. If this is your first exposure to Go, please see +[Tutorial: Get started with Go](/doc/tutorial/getting-started) +for a quick introduction. + +Gin simplifies many coding tasks associated with building web applications, +including web services. In this tutorial, you'll use Gin to route requests, +retrieve request details, and marshal JSON for responses. + +In this tutorial, you will build a RESTful API server with two endpoints. Your +example project will be a repository of data about vintage jazz records. + +The tutorial includes the following sections: + +1. Design API endpoints. +2. Create a folder for your code. +3. Create the data. +4. Write a handler to return all items. +5. Write a handler to add a new item. +6. Write a handler to return a specific item. + +**Note:** For other tutorials, see [Tutorials](/doc/tutorial/index.html). + +## Prerequisites + +* **An installation of Go 1.16 or later.** For installation instructions, see + [Installing Go](/doc/install). +* **A tool to edit your code.** Any text editor you have will work fine. +* **A command terminal.** Go works well using any terminal on Linux and Mac, + and on PowerShell or cmd in Windows. +* **The curl tool.** On Linux and Mac, this should already be installed. On + Windows, it's included on Windows 10 Insider build 17063 and later. For earlier + Windows versions, you might need to install it. For more, see + [Tar and Curl Come to Windows](https://docs.microsoft.com/en-us/virtualization/community/team-blog/2017/20171219-tar-and-curl-come-to-windows). + +## Design API endpoints {#design_endpoints} + +You'll build an API that provides access to a store selling vintage recordings +on vinyl. So you'll need to provide endpoints through which a client can get +and add albums for users. + +When developing an API, you typically begin by designing the endpoints. Your +API's users will have more success if the endpoints are easy to understand. + +Here are the endpoints you'll create in this tutorial. + +/albums +* `GET` – Get a list of all albums, returned as JSON. +* `POST` – Add a new album from request data sent as JSON. + +/albums/:id +* `GET` – Get an album by its ID, returning the album data as JSON. + +Next, you'll create a folder for your code. + +## Create a folder for your code {#create_folder} + +To begin, create a project for the code you'll write. + +1. Open a command prompt and change to your home directory. + + On Linux or Mac: + + ``` + $ cd + ``` + + On Windows: + + ``` + C:\> cd %HOMEPATH% + ``` + +2. Using the command prompt, create a directory for your code called + web-service-gin. + + ``` + $ mkdir web-service-gin + $ cd web-service-gin + ``` + +3. Create a module in which you can manage dependencies. + + Run the `go mod init` command, giving it the path of the module your code + will be in. + + ``` + $ go mod init example.com/web-service-gin + go: creating new go.mod: module example.com/web-service-gin + ``` + + This command creates a go.mod file in which dependencies you add will be + listed for tracking. For more, be sure to see + [Managing dependencies](/doc/modules/managing-dependencies). + +Next, you'll design data structures for handling data. + +## Create the data {#create_data} + +To keep things simple for the tutorial, you'll store data in memory. A more +typical API would interact with a database. + +Note that storing data in memory means that the set of albums will be lost each +time you stop the server, then recreated when you start it. + +#### Write the code + +1. Using your text editor, create a file called main.go in the web-service + directory. You'll write your Go code in this file. +2. Into main.go, at the top of the file, paste the following package declaration. + + ``` + package main + ``` + + A standalone program (as opposed to a library) is always in package `main`. + +3. Beneath the package declaration, paste the following declaration of an + `album` struct. You'll use this to store album data in memory. + + Struct tags such as ``json:"artist"`` specify what a field's name should be + when the struct's contents are serialized into JSON. Without them, the JSON + would use the struct's capitalized field names – a style not as common in + JSON. + + ``` + // album represents data about a record album. + type album struct { + ID string `json:"id"` + Title string `json:"title"` + Artist string `json:"artist"` + Price float64 `json:"price"` + } + ``` + +4. Beneath the struct declaration you just added, paste the following slice of + `album` structs containing data you'll use to start. + + ``` + // albums slice to seed record album data. + var albums = []album{ + {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, + {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, + {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, + } + ``` + +Next, you'll write code to implement your first endpoint. + +## Write a handler to return all items {#all_items} + +When the client makes a request at `GET /albums`, you want to return all the +albums as JSON. + +To do this, you'll write the following: + +* Logic to prepare a response +* Code to map the request path to your logic + +Note that this is the reverse of how they'll be executed at runtime, but you're +adding dependencies first, then the code that depends on them. + +#### Write the code + +1. Beneath the struct code you added in the preceding section, paste the + following code to get the album list. + + This `getAlbums` function creates JSON from the slice of `album` structs, + writing the JSON into the response. + + ``` + // getAlbums responds with the list of all albums as JSON. + func getAlbums(c *gin.Context) { + c.IndentedJSON(http.StatusOK, albums) + } + ``` + + In this code, you: + + * Write a `getAlbums` function that takes a + [`gin.Context`](https://pkg.go.dev/github.com/gin-gonic/gin#Context) + parameter. Note that you could have given this function any name – neither + Gin nor Go require a particular function name format. + + `gin.Context` is the most important part of Gin. It carries request + details, validates and serializes JSON, and more. (Despite the similar + name, this is different from Go's built-in + [`context`](https://golang.org/pkg/context/) package.) + + * Call [`Context.IndentedJSON`](https://pkg.go.dev/github.com/gin-gonic/gin#Context.IndentedJSON) + to serialize the struct into JSON and add it to the response. + + The function's first argument is the HTTP status code you want to send to + the client. Here, you're passing the [`StatusOK`](https://pkg.go.dev/net/http#StatusOK) + constant from the `net/http` package to indicate `200 OK`. + + Note that you can replace `Context.IndentedJSON` with a call to + [`Context.JSON`](https://pkg.go.dev/github.com/gin-gonic/gin#Context.JSON) + to send more compact JSON. In practice, the indented form is much easier to + work with when debugging and the size difference is usually small. + +2. Near the top of main.go, just beneath the `albums` slice declaration, paste + the code below to assign the handler function to an endpoint path. + + This sets up an association in which `getAlbums` handles requests to the + `/albums` endpoint path. + + ``` + func main() { + router := gin.Default() + router.GET("/albums", getAlbums) + + router.Run("localhost:8080") + } + ``` + + In this code, you: + + * Initialize a Gin router using + [`Default`](https://pkg.go.dev/github.com/gin-gonic/gin#Default). + * Use the [`GET`](https://pkg.go.dev/github.com/gin-gonic/gin#RouterGroup.GET) + function to associate the `GET` HTTP method and `/albums` path with a handler + function. + + Note that you're passing the _name_ of the `getAlbums` function. This is + different from passing the _result_ of the function, which you would do by + passing `getAlbums()` (note the parenthesis). + + * Use the [`Run`](https://pkg.go.dev/github.com/gin-gonic/gin#Engine.Run) + function to attach the router to an `http.Server` and start the server. + +3. Near the top of main.go, just beneath the package declaration, import the + packages you'll need to support the code you've just written. + + The first lines of code should look like this: + + ``` + package main + + import ( + "net/http" + + "github.com/gin-gonic/gin" + ) + ``` + +4. Save main.go. + +#### Run the code + +1. Begin tracking the Gin module as a dependency. + + At the command line, use [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) + to add the github.com/gin-gonic/gin module as a dependency for your module. + Use a dot argument to mean "get dependencies for code in the current + directory." + + ``` + $ go get . + go get: added github.com/gin-gonic/gin v1.7.2 + ``` + + Go resolved and downloaded this dependency to satisfy the `import` + declaration you added in the previous step. + +2. From the command line in the directory containing main.go, run the code. + Use a dot argument to mean "run code in the current directory." + + ``` + $ go run . + ``` + + Once the code is running, you have a running HTTP server to which you can + send requests. + +3. From a new command line window, use `curl` to make a request to your running + web service. + + ``` + $ curl http://localhost:8080/albums + ``` + + The command should display the data you seeded the service with. + + ``` + [ + { + "id": "1", + "title": "Blue Train", + "artist": "John Coltrane", + "price": 56.99 + }, + { + "id": "2", + "title": "Jeru", + "artist": "Gerry Mulligan", + "price": 17.99 + }, + { + "id": "3", + "title": "Sarah Vaughan and Clifford Brown", + "artist": "Sarah Vaughan", + "price": 39.99 + } + ] + ``` + +You've started an API! In the next section, you'll create another endpoint with +code to handle a `POST` request to add an item. + +## Write a handler to add a new item {#add_item} + +When the client makes a `POST` request at `/albums`, you want to add the album +described in the request body to the existing albums data. + +To do this, you'll write the following: + +* Logic to add the new album to the existing list. +* A bit of code to route the `POST` request to your logic. + +#### Write the code + +1. Add code to add albums data to the list of albums. + + Somewhere after the `import` statements, paste the following code. (The end + of the file is a good place for this code, but Go doesn't enforce the order + in which you declare functions.) + + ``` + // postAlbums adds an album from JSON received in the request body. + func postAlbums(c *gin.Context) { + var newAlbum album + + // Call BindJSON to bind the received JSON to + // newAlbum. + if err := c.BindJSON(&newAlbum); err != nil { + return + } + + // Add the new album to the slice. + albums = append(albums, newAlbum) + c.IndentedJSON(http.StatusCreated, newAlbum) + } + ``` + + In this code, you: + + * Use [`Context.BindJSON`](https://pkg.go.dev/github.com/gin-gonic/gin#Context.BindJSON) + to bind the request body to `newAlbum`. + * Append the `album` struct initialized from the JSON to the `albums` + slice. + * Add a `201` status code to the response, along with JSON representing + the album you added. + +2. Change your `main` function so that it includes the `router.POST` function, + as in the following. + + ``` + func main() { + router := gin.Default() + router.GET("/albums", getAlbums) + router.POST("/albums", postAlbums) + + router.Run("localhost:8080") + } + ``` + + In this code, you: + + * Associate the `POST` method at the `/albums` path with the `postAlbums` + function. + + With Gin, you can associate a handler with an HTTP method-and-path + combination. In this way, you can separately route requests sent to a + single path based on the method the client is using. + +#### Run the code + +1. If the server is still running from the last section, stop it. +2. From the command line in the directory containing main.go, run the code. + + ``` + $ go run . + ``` + +3. From a different command line window, use `curl` to make a request to your + running web service. + + ``` + $ curl http://localhost:8080/albums \ + --include \ + --header "Content-Type: application/json" \ + --request "POST" \ + --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}' + ``` + + The command should display headers and JSON for the added album. + + ``` + HTTP/1.1 201 Created + Content-Type: application/json; charset=utf-8 + Date: Wed, 02 Jun 2021 00:34:12 GMT + Content-Length: 116 + + { + "id": "4", + "title": "The Modern Sound of Betty Carter", + "artist": "Betty Carter", + "price": 49.99 + } + ``` + +4. As in the previous section, use `curl` to retrieve the full list of albums, + which you can use to confirm that the new album was added. + + ``` + $ curl http://localhost:8080/albums \ + --header "Content-Type: application/json" \ + --request "GET" + ``` + + The command should display the album list. + + ``` + [ + { + "id": "1", + "title": "Blue Train", + "artist": "John Coltrane", + "price": 56.99 + }, + { + "id": "2", + "title": "Jeru", + "artist": "Gerry Mulligan", + "price": 17.99 + }, + { + "id": "3", + "title": "Sarah Vaughan and Clifford Brown", + "artist": "Sarah Vaughan", + "price": 39.99 + }, + { + "id": "4", + "title": "The Modern Sound of Betty Carter", + "artist": "Betty Carter", + "price": 49.99 + } + ] + ``` + +In the next section, you'll add code to handle a `GET` for a specific item. + +## Write a handler to return a specific item {#specific_item} + +When the client makes a request to `GET /albums/[id]`, you want to return the +album whose ID matches the `id` path parameter. + +To do this, you will: + +* Add logic to retrieve the requested album. +* Map the path to the logic. + +#### Write the code + +1. Beneath the `postAlbums` function you added in the preceding section, paste + the following code to retrieve a specific album. + + This `getAlbumByID` function will extract the ID in the request path, then + locate an album that matches. + + ``` + // getAlbumByID locates the album whose ID value matches the id + // parameter sent by the client, then returns that album as a response. + func getAlbumByID(c *gin.Context) { + id := c.Param("id") + + // Loop over the list of albums, looking for + // an album whose ID value matches the parameter. + for _, a := range albums { + if a.ID == id { + c.IndentedJSON(http.StatusOK, a) + return + } + } + c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"}) + } + ``` + + In this code, you: + + * Use [`Context.Param`](https://pkg.go.dev/github.com/gin-gonic/gin#Context.Param) + to retrieve the `id` path parameter from the URL. When you map this + handler to a path, you'll include a placeholder for the parameter in the + path. + * Loop over the `album` structs in the slice, looking for one whose `ID` + field value matches the `id` parameter value. If it's found, you serialize + that `album` struct to JSON and return it as a response with a `200 OK` + HTTP code. + + As mentioned above, a real-world service would likely use a database + query to perform this lookup. + + * Return an HTTP `404` error with [`http.StatusNotFound`](https://pkg.go.dev/net/http#StatusNotFound) + if the album isn't found. + +2. Finally, change your `main` so that it includes a new call to `router.GET`, + where the path is now `/albums/:id`, as shown in the following example. + + ``` + func main() { + router := gin.Default() + router.GET("/albums", getAlbums) + router.GET("/albums/:id", getAlbumByID) + router.POST("/albums", postAlbums) + + router.Run("localhost:8080") + } + ``` + + In this code, you: + + * Associate the `/albums/:id` path with the `getAlbumByID` function. In + Gin, the colon preceding an item in the path signifies that the item is + a path parameter. + +#### Run the code + +1. If the server is still running from the last section, stop it. +2. From the command line in the directory containing main.go, run the code to + start the server. + + ``` + $ go run . + ``` + +3. From a different command line window, use `curl` to make a request to your + running web service. + + ``` + $ curl http://localhost:8080/albums/2 + ``` + + The command should display JSON for the album whose ID you used. If the + album wasn't found, you'll get JSON with an error message. + + ``` + { + "id": "2", + "title": "Jeru", + "artist": "Gerry Mulligan", + "price": 17.99 + } + ``` + +## Conclusion {#conclusion} + +Congratulations! You've just used Go and Gin to write a simple RESTful web +service. + +Suggested next topics: + +* If you're new to Go, you'll find useful best practices described in + [Effective Go](/doc/effective_go) and + [How to write Go code](/doc/code). +* The [Go Tour](https://tour.golang.org/welcome/1) is a great step-by-step + introduction to Go fundamentals. +* For more about Gin, see the [Gin Web Framework package documentation](https://pkg.go.dev/github.com/gin-gonic/gin) + or the [Gin Web Framework docs](https://gin-gonic.com/docs/). + +## Completed code {#completed_code} + +This section contains the code for the application you build with this tutorial. + +``` +package main + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// album represents data about a record album. +type album struct { + ID string `json:"id"` + Title string `json:"title"` + Artist string `json:"artist"` + Price float64 `json:"price"` +} + +// albums slice to seed record album data. +var albums = []album{ + {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, + {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, + {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, +} + +func main() { + router := gin.Default() + router.GET("/albums", getAlbums) + router.GET("/albums/:id", getAlbumByID) + router.POST("/albums", postAlbums) + + router.Run("localhost:8080") +} + +// getAlbums responds with the list of all albums as JSON. +func getAlbums(c *gin.Context) { + c.IndentedJSON(http.StatusOK, albums) +} + +// postAlbums adds an album from JSON received in the request body. +func postAlbums(c *gin.Context) { + var newAlbum album + + // Call BindJSON to bind the received JSON to + // newAlbum. + if err := c.BindJSON(&newAlbum); err != nil { + return + } + + // Add the new album to the slice. + albums = append(albums, newAlbum) + c.IndentedJSON(http.StatusCreated, newAlbum) +} + +// getAlbumByID locates the album whose ID value matches the id +// parameter sent by the client, then returns that album as a response. +func getAlbumByID(c *gin.Context) { + id := c.Param("id") + + // Loop through the list of albums, looking for + // an album whose ID value matches the parameter. + for _, a := range albums { + if a.ID == id { + c.IndentedJSON(http.StatusOK, a) + return + } + } + c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"}) +} +``` |
