tutorial: add web service tutorial using Go and Gin This adds an introductory tutorial on building a small RESTful API using Go and the Gin web framework. This change includes content on: - Using the Gin web framework (https://pkg.go.dev/github.com/gin-gonic/gin). - Creating an API that includes three HTTP method-and-endpoint combinations. - GET and POST actions that convert between a Go struct and JSON. - Supporting URL path parameters. - Go command-line tools such as `go get` and `go run`. This content is written in the enhanced Markdown format supported by the interactive tutorial application in the Google Cloud Shell Editor. For more, see https://cloud.google.com/shell/docs/cloud-shell-tutorials/tutorials Change-Id: I216ec12763af83db257c978997a2987eae418995 Reviewed-on: https://go-review.googlesource.com/c/tour/+/328611 Trust: Steve Traut <straut@google.com> Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/tutorial/web-service-gin.md b/tutorial/web-service-gin.md new file mode 100644 index 0000000..b471cb7 --- /dev/null +++ b/tutorial/web-service-gin.md
@@ -0,0 +1,655 @@ +--- +title: Developing a RESTful API with Go and Gin +description: Using the Gin web framework to develop a RESTful API. +author: straut@google.com +tags: Go +date_published: +--- + +# 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](https://golang.org/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. + +## Design API 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 project for your code. + +## Create a project for your code + +To begin, create a project for the code you'll write. + +1. Ensure that the **cloudshell_open** folder is selected. + +2. Click the <walkthrough-editor-spotlight spotlightId="menu-file">File Menu</walkthrough-editor-spotlight>, + then click **New Folder**. + +3. In the **New Folder** dialog, enter `web-service-gin` for the folder name, + then click **OK**. + +4. Click the <walkthrough-editor-spotlight spotlightId="menu-file">File Menu</walkthrough-editor-spotlight>, + then click **Open Workspace**. + +5. In the **Open Workspace** dialog, select the cloudshell_open/web-service-gin + folder you just created, then click **Open**. + +6. Click the <walkthrough-editor-spotlight spotlightId="menu-terminal-new-terminal">New Terminal</walkthrough-editor-spotlight> + menu command to open a new Cloud Shell terminal. + + The terminal prompt should be in the web-service-gin directory. + +7. 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. + + ```bash + go mod init example.com/web-service-gin + go: creating new go.mod: module example.com/web-service-gin + ``` + + This command creates a <walkthrough-editor-spotlight spotlightId="navigator" spotlightItem="go.mod">go.mod file</walkthrough-editor-spotlight> + in which dependencies you might add will be listed for tracking. For more, + be sure to see [Managing dependencies](https://golang.org/doc/modules/managing-dependencies). + +Next, you'll design data structures for handling data. + +## Create the 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. Click the <walkthrough-editor-spotlight spotlightId="menu-file">File Menu</walkthrough-editor-spotlight>, then click **New File**. + +2. In the **New File** dialog, enter `main.go` for the file name, then click **OK**. + +3. Into main.go, at the top of the file, paste the package declaration below. + + ``` + package main + ``` + + A standalone program (as opposed to a library) is always in package `main`. + +4. 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"` + } + ``` + +5. 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 + +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 + +As you follow these steps, ignore the errors visible in the editor. You'll fix +these in **Run the code**, below. + +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." + + ```bash + 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. In the Cloud Shell terminal window, run the code. + + Use a dot argument to mean "run the package in the current directory." + + ```bash + go run . + ``` + + Once the code is running, you have a running HTTP server to which you can + send requests. + +3. Open a new terminal window in which to make requests to the running web service. + + Click the <walkthrough-editor-spotlight spotlightId="menu-terminal-new-terminal">New Terminal</walkthrough-editor-spotlight> + menu command to open a new Cloud Shell terminal. + +4. In the second Cloud Shell terminal window, use `curl` to make a request to your + running web service. + + ```bash + 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 + +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. + +4. Save main.go. + +### Run the code + +1. In the first Cloud Shell terminal window, if the server is still running from the last section, stop it. + +2. In the first Cloud Shell terminal window, run the code. + + ```bash + go run . + ``` + +3. In the other Cloud Shell terminal window, use the following `curl` command to make + a request to your running web service. + + ```bash + 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. + + ```bash + 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 + +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. In the first Cloud Shell terminal window, if the server is still running from the last section, stop it. + +2. In the first Cloud Shell terminal window, run the code. + + ```bash + go run . + ``` + +3. In the other Cloud Shell terminal window, use `curl` to make a request to your + running web service. + + ```bash + 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 + } + ``` + +Continue to the last section for links to useful content. + +## 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](https://golang.org/doc/effective_go) and + [How to write Go code](https://golang.org/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/). + +Continue to the next section to view the full code for the application you build +with this tutorial. + +## 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"}) +} +```