Go × Gin Basics: A Thorough Guide to Building a High-speed API Server

  • gin
    gin
  • golang
    golang
Published on 2023/11/23

Introduction

The Go language boasts simplicity and high-speed processing capabilities, making it a very reliable choice for web services and API development. The lightweight and high-performance web framework “Gin” helps you draw out Go’s strengths to the fullest.

In this article, we’ll walk through the appeal of Go × Gin and the basic usage in a structured way so that even beginners can follow along easily. We’ll move from installation to creating a Hello World, the basics of requests and responses, and then on to using middleware, with concrete examples you can try right away.

If you’re thinking “I want to build a fast and simple API server,” I hope this article will serve as a helpful guide. Let’s take a look into the world of Gin together.

Why Choose Go + Gin

When building a web application or API server, the language and framework you use are extremely important. Let's organize the reasons for choosing Go and Gin here.

1️⃣ The appeal of Go

  • High performance: Go is a compiled language, enabling lightweight and fast execution.
  • Simple syntax: You can write robust code with minimal notation, which keeps learning costs low.
  • Strong in concurrency: By leveraging Goroutines and Channels, it’s easy to implement scalable processing.

2️⃣ The strengths of Gin

  • Astonishing speed: Gin is known as an “extremely fast web framework.”
  • Rich features: It covers all the features needed for an API server, such as routing, middleware, and JSON handling.
  • Simple API design: With an easy-to-understand API, you can quickly jump into practical development.

3️⃣ The great compatibility of Go × Gin

Go’s simplicity and Gin’s speed and functionality are highly compatible, supporting the construction of API servers that balance performance and maintainability.
In this article, we’ll make use of this powerful combination and carefully explain the flow of actually building an API server. Next, we’ll start by getting Gin up and running.

Installing Gin and Initial Setup

Here, we’ll install Gin into a project and go as far as running a minimal server.

Checking your Go version

First, check your Go version. Gin recommends Go 1.18 or later, so use the following command to check your current Go version.

go version

Example:

go version go1.23.0 darwin/arm64

Creating the project directory

Create a directory for your new project. As an example, we’ll create a directory named my-gin-app.

mkdir go-gin-basic-guide
cd go-gin-basic-guide

Initializing the Go module

If you haven’t initialized a Go module yet, run the following:

go mod init go-gin-basic-guide

Installing Gin

Install Gin.

go get -u github.com/gin-gonic/gin

Image from Gyazo

Creating a minimal server

Create main.go and write the following content.

main.go
package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // Default port :8080
}

Starting the server

Start the server with the following command:

go run main.go

Image from Gyazo

Access http://localhost:8080/ping in your browser or with curl and confirm that a JSON response is displayed.

curl http://localhost:8080/ping

Image from Gyazo

With this, the basic preparation to run Gin is complete. Next, we’ll dig a bit deeper into Gin’s basic structure.

Note: Enabling hot reload with Air

During development, manually running go run every time you change code is tedious. To solve this, we’ll introduce air to enable hot reload.

Install air with the following command:

go install github.com/air-verse/air@latest

air can use a configuration file named .air.toml. Create it in the project root with the following content:

# .air.toml
root = "."
tmp_dir = "tmp"

[build]
  cmd = "go build -o ./tmp/main ."
  bin = "tmp/main"
  include_ext = ["go"]
  exclude_dir = ["tmp", "vendor"]

[log]
  time = true
Item Meaning
root Root directory to watch
tmp_dir Location to place build artifacts
[build] Build and execution settings
cmd Build command (here, builds the Go binary to tmp/main)
bin Path to the binary file to execute
include_ext File extensions to watch
exclude_dir Directories to exclude from watching

Run the following command and air will watch for source code changes and restart immediately.

air

Image from Gyazo

As a test, change the URL path from /ping -> /pong.

main.go
func main() {
	r := gin.Default()
-	r.GET("/ping", func(c *gin.Context) {
+	r.GET("/pong", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // Default :8080 port
}

You can then confirm that a response is returned at /pong.

curl http://localhost:8080/pong

After you finish checking the behavior, revert the change.

Understanding Gin’s Basic Structure

A Gin application is structured around the router, handlers, and context. Here, we'll organize the role and mechanism of each, and add examples where you can actually change requests and check how they behave.

Router

The entry point of Gin is the “router.” Using gin.Default() lets you easily initialize a router with logger and recovery middleware already built in.

r := gin.Default()

Handler

Handler functions correspond to the endpoints registered in the router. They receive HTTP requests and return responses.

For example, the following returns a JSON response “pong” when accessing the /ping path.

r.GET("/ping", func(c *gin.Context) {
    c.JSON(200, gin.H{"message": "pong"})
})

Here, /ping is the URL path part. Access http://localhost:8080/ping in your browser or with curl to check how it works.

Context

*gin.Context is a convenient struct for handling information related to requests and responses.

For example, you can get query parameters like this:

main.go
func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
+	r.GET("/hello", func(c *gin.Context) {
+		name := c.Query("name") // Get with ?name=value
+		c.JSON(200, gin.H{"message": "Hello, " + name})
+	})
	r.Run() // Default :8080 port
}
curl 'http://localhost:8080/hello?name=Gopher'

The response will be:

{"message": "Hello, Gopher"}

If you change the query parameter and send the request again, you can confirm that the response also changes.

Grouping

When you have many endpoints, you’ll want to manage common paths together. With Gin’s grouping, you can group common parts of paths.

In the example below, we group the common path /api and define GET /api/users and POST /api/users inside it.

main.go
func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.GET("/hello", func(c *gin.Context) {
		name := c.Query("name") // ?name=value
		c.JSON(200, gin.H{"message": "Hello, " + name})
	})
+	api := r.Group("/api")
+	{
+		api.GET("/users", func(c *gin.Context) {
+			c.JSON(200, gin.H{"message": "GET /api/users"})
+		})
+
+		api.POST("/users", func(c *gin.Context) {
+			c.JSON(200, gin.H{"message": "POST /api/users"})
+		})
+	}
	r.Run() // Default :8080 port
}

Access the following URLs to confirm that each endpoint is working correctly.

curl http://localhost:8080/api/users
curl -X POST http://localhost:8080/api/users

Basics of Request Handling

Here, we'll organize the basics of request handling with Gin.
We’ll look at how to actually get parameters and return responses while checking the behavior.

Getting path parameters

To get values embedded in the path, define routing in the form :param.

main.go
+ r.GET("/users/:id", func(c *gin.Context) {
+     id := c.Param("id")
+     c.JSON(200, gin.H{"user_id": id})
+ })

Access the following URL:

curl http://localhost:8080/users/123

Response

{"user_id": "123"}

Getting query parameters

To get query parameters (in the form ?key=value), use c.Query.

main.go
+ r.GET("/search", func(c *gin.Context) {
+     keyword := c.Query("q")
+     c.JSON(200, gin.H{"query": keyword})
+ })

Access the following URL:

curl 'http://localhost:8080/search?q=Gin'

Response

{"query": "Gin"}

You can confirm that changing the query parameter dynamically changes the response.

Getting form parameters (POST)

Data sent from a form can be obtained with c.PostForm.

main.go
+ r.POST("/submit", func(c *gin.Context) {
+     name := c.PostForm("name")
+     c.JSON(200, gin.H{"submitted_name": name})
+ })

Send a POST request with curl.

curl -X POST -d "name=Gopher" http://localhost:8080/submit

Response

{"submitted_name": "Gopher"}

Getting JSON requests

When the client sends data in JSON format, use c.BindJSON to bind it to a struct.

main.go
+ type User struct {
+     Name string `json:"name"`
+     Age  int    `json:"age"`
+ }

func main() {
	r := gin.Default()

+ r.POST("/json", func(c *gin.Context) {
+		var user User
+		if err := c.BindJSON(&user); err != nil {
+			c.JSON(400, gin.H{"error": err.Error()})
+			return
+		}
+		c.JSON(200, gin.H{"name": user.Name, "age": user.Age})
+	})
	r.Run() // Default :8080 port
}

Send JSON with curl.

curl -X POST -H "Content-Type: application/json" -d '{"name":"Gopher","age":5}' http://localhost:8080/json

Response

{"name": "Gopher", "age": 5}

Summary

As the basics of request handling, we covered the following points:

✅ Path parameters: c.Param
✅ Query parameters: c.Query
✅ Form parameters: c.PostForm
✅ JSON requests: c.BindJSON

By combining these, you can flexibly accept various API requests.

Next, we’ll look at how to write responses.

How to Write Responses

Here, we'll organize how to write responses using Gin.
We’ll clarify the most important part of an API server: “what to return to the client.”

JSON responses

In APIs, it’s common to return responses in JSON format. Use c.JSON to return them as follows:

r.GET("/json", func(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "Hello, Gin!",
    })
})
  • First argument: HTTP status code (e.g., 200)
  • Second argument: JSON data to return (map[string]interface{} or a struct)

String responses

To return text directly, use c.String.

r.GET("/text", func(c *gin.Context) {
    c.String(200, "This is a plain text response.")
})

This is useful for returning simple messages.

HTML responses

To return HTML, use c.HTML.
Here’s an example that loads templates with LoadHTMLGlob and returns HTML.

main.go
+ r.LoadHTMLGlob("templates/*")
+ r.GET("/html", func(c *gin.Context) {
+     c.HTML(200, "index.tmpl", gin.H{"title": "Hello, HTML!"})
+ })

Use the following directory structure:

my-gin-app/
├── main.go
└── templates/
    └── index.tmpl

Sample templates/index.tmpl:

<!DOCTYPE html>
<html>
<head>
  <title>{{ .title }}</title>
</head>
<body>
  <h1>{{ .title }}</h1>
  <p>This is a sample HTML response rendered by Gin.</p>
</body>
</html>

In the browser, you’ll see a heading and body text that say Hello, HTML!.

Image from Gyazo

Summary

As for how to write responses with Gin, we covered the following points:

✅ JSON: c.JSON
✅ String: c.String
✅ HTML: c.HTML
✅ Status code only: c.Status
✅ Unified error responses: define a struct and return it

By using these appropriately, you can design and operate your APIs smoothly.

Next, we’ll look at how to use Gin’s middleware.

Using Middleware

In Gin, you can use middleware to insert common processing before and after requests.
Here, we'll organize how to introduce the built-in middleware and your own custom middleware.

Built-in middleware

Gin provides several convenient built-in middleware. Here are some representative ones.

Logger
Middleware that logs requests.

r := gin.New()
r.Use(gin.Logger())

Since gin.Default() already includes the logger, you don’t need to add it again.

When you access the API, logs will be output to the console.

Image from Gyazo

Recovery
Middleware that prevents the server from crashing when a panic occurs and returns a 500 error response instead.

r.Use(gin.Recovery())

This is also included in gin.Default().

How to create custom middleware

If you want to perform common pre-processing or post-processing, you can create your own middleware.

Below is a simple example that prints “Before request...” every time a request comes in.

main.go
+ func MyCustomMiddleware() gin.HandlerFunc {
+ 	return func(c *gin.Context) {
+ 		println("Before request...")
+ 		c.Next()
+ 		println("After request...")
+ 	}
+ }

func main() {
	r := gin.Default()
+ 	r.Use(MyCustomMiddleware())

When you send a request to the API, you can confirm that “Before request...” and “After request...” are printed.

Image from Gyazo

You can also set middleware only on specific routes instead of applying it globally.

main.go
func main() {
	r := gin.Default()
- 	r.Use(MyCustomMiddleware())

+	r.GET("/special", MyCustomMiddleware(), func(c *gin.Context) {
+		c.JSON(200, gin.H{"message": "This route uses a custom middleware."})
+	})

	r.Run() // Default :8080 port
}

You can confirm that only specific routes print “Before request...” and “After request...”.

Image from Gyazo

In Closing

So far, we’ve gone through the basics of developing an API server in Go using Gin.
By actually writing code and running it, you should have organized the following points:

✅ Gin’s basic structure (router, handler, context)
✅ How to get various request parameters
✅ How to write JSON, text, and HTML responses
✅ How to use and manage middleware

All of these are foundational concepts for API development. The accumulation of small features leads to large systems in the future.

Moving on to the next step

Based on the basic knowledge you learned this time, next we’ll move on to more practical, applied topics.

  • Implementing authentication and validation
  • Testing techniques with Gin
  • Key points for production operation
  • Thinking about scalable architectures, etc.
Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form