Go × Gin Basics: A Thorough Guide to Building a High-speed API Server
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
Creating a minimal server
Create main.go and write the following content.
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
Access http://localhost:8080/ping in your browser or with curl and confirm that a JSON response is displayed.
curl http://localhost:8080/ping
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
As a test, change the URL path from /ping -> /pong.
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:
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.
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.
+ 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.
+ 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.
+ 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.
+ 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.
+ 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!.
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.
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.
+ 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.
You can also set middleware only on specific routes instead of applying it globally.
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...”.
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.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Complete Cache Strategy Guide: Maximizing Performance with CDN, Redis, and API Optimization
2024/03/07Getting Started with Building Web Servers Using Go × Echo: Learn Simple, High‑Performance API Development in the Shortest Time
2023/12/03Go × Gin Advanced: Practical Techniques and Scalable API Design
2023/11/29Article & Like API with Go + Gin + GORM (Part 1): First, an "implementation-first controller" with all-in-one CRUD
2025/07/13Building an MVC-Structured Blog Post Web API with Go × Gin: From Basics to Scalable Design
2023/12/03Robust Test Design and Implementation Guide in a Go × Gin × MVC Architecture
2023/12/04Bringing a Go + Gin App Up to Production Quality: From Configuration and Structure to CI
2023/12/06Released a string slice utility for Go: “strlistutils”
2025/06/19







