Gin Basics #1 Getting Started and Your First Server
This series is a 7-part introductory course for anyone who wants to build a web API in Go. If you picked up the syntax in the Go Basics series, this is the step where you use that syntax to write a real HTTP server.
- #1 Getting started and your first server ← this post
- #2 Routing and handlers
- #3 Request binding and validation
- #4 Responses — JSON, status codes, errors
- #5 Middleware
- #6 Database integration (GORM)
- #7 Project structure and a mini REST API
This post points out where the standard library net/http alone falls short, then takes you through installing Gin and running your first server.
What is Gin? #
Gin is the most widely used web framework in Go. More precisely, it’s a lightweight framework centered on an HTTP router and middleware. It sits as a thin layer on top of the standard library net/http, so it keeps Go’s default behavior almost intact while cutting down on the repetitive work.
Its key characteristics:
- Fast routing — a radix tree based router for fast path matching
- Concise handlers — a single
gin.Contexthandles both reading the request and writing the response - Middleware chain — cleanly wires up common processing like logging, recovery, and authentication
- Convenience features — JSON binding, validation, and file responses come built in
Since it’s effectively the de facto standard choice in the Go community, it has the most resources and examples. Being able to easily find answers when you get stuck is a big advantage too.
Why isn’t net/http alone enough? #
Go can write an HTTP server with just the standard library. As of Go 1.22, the ServeMux in net/http supports method and path pattern matching. So for a simple server, you don’t need an external framework at all.
But as you build a real API, you end up repeating the same code over and over.
package main
import (
"encoding/json"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /ping", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "pong"})
})
http.ListenAndServe(":8080", mux)
}Even just sending out one JSON response, you have to set the header, the status code, and the encoding by hand every time. Add reading path parameters, validating the request body, and applying common logic across multiple handlers, and the code gets verbose fast.
Gin reduces exactly this repetition. The same response written with Gin looks like this.
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})A single line, c.JSON, handles the header, status code, and encoding all at once. The difference looks small, but once your endpoints grow to dozens, the accumulated difference is large.
Compared to other Go web frameworks #
There are options besides Gin, so let me sketch out roughly where each sits.
| Gin | Echo | Fiber | Chi | |
|---|---|---|---|---|
| Base | net/http | net/http | fasthttp | net/http |
| Character | Router + middleware | Router + middleware | Express style | Thin router |
| Ecosystem | Largest | Large | Large | Standard-friendly |
| Learning curve | Low | Low | Low | Lowest |
Echo sits in almost the same position as Gin. Fiber sits on top of fasthttp rather than net/http, gaining more performance at the cost of giving up some standard compatibility. Chi is the thin router most faithful to the standard net/http. If you’re just starting out, I recommend you start with Gin, which has the most resources, and look at the other options when you need them.
Preparation — Go modules #
Gin is a Go package, so you need Go installed first. If you haven’t installed it yet, read Go Basics #1 first. This series assumes Go 1.22 or later.
Create a new project and start a module.
mkdir gin-hello
cd gin-hello
go mod init gin-hellogo mod init creates a go.mod file and starts a new module. The module concept was covered in the Go Basics series, so here we’ll just move ahead with it.
Installing Gin #
Add Gin to your dependencies with go get.
go get github.com/gin-gonic/ginThis series is written against Gin v1.10. Installing it records the dependency in go.mod and leaves a checksum in go.sum. Include both in version control (git).
Your first server — Hello, Gin #
Create a main.go file with the following.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
r.Run() // default :8080
}Run it.
go run main.goOnce the server is up, send a request from another terminal.
curl http://localhost:8080/ping
# {"message":"pong"}Opening http://localhost:8080/ping in a browser shows the same JSON. Your first Gin server is working.
Walking through the code line by line #
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
r.Run()gin.Default()— creates the router (engine). The logging and recovery middleware are attached by default.r.GET("/ping", ...)— registers the handler to run when a GET request comes in on the/pingpath.func(c *gin.Context)— the shape of a handler. All request information and response capabilities are packed into a singlegin.Context.c.JSON(...)— takes a status code and data and responds as JSON.gin.His shorthand formap[string]any.r.Run()— starts the server. With no argument it listens on:8080. You can also specify a port, liker.Run(":3000").
Notice that we used http.StatusOK instead of a bare number like 200 for the status code. The meaning becomes clear, and the risk of entering the wrong code due to a typo goes down. This series follows the practice of using the standard constants.
gin.Default() vs gin.New() #
There are two ways to create a router.
r := gin.Default() // includes Logger + Recovery middleware
r := gin.New() // an empty engine with no middlewaregin.Default() automatically attaches the Logger middleware, which prints request logs, and the Recovery middleware, which keeps the server from dying even when a handler panics. For the introductory stage, gin.Default() is plenty.
gin.New() is an empty engine with no middleware at all. Use it when you want to directly control which middleware to attach. Middleware is covered in detail in #5 Middleware.
Release mode #
The first time you start the server, you’ll see the following warning in the console.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.Gin defaults to debug mode, so it prints debug information like the list of registered routes. In a real deployment environment, you switch to release mode to cut down the unnecessary logs.
gin.SetMode(gin.ReleaseMode)It can also be set via the environment variable GIN_MODE=release. For now, since you’re in development, it’s fine to leave it in debug mode. Deployment-related settings will be revisited in the intermediate series.
Hot reload — auto restart on save #
Re-running go run every time you edit your code is tedious. A tool like air automatically rebuilds and restarts the server whenever a file changes.
go install github.com/air-verse/air@latest
airIt isn’t required, but it greatly improves the development experience. You can follow along with this series fine even without installing it.
Wrapping up #
What this post covered:
- Gin is the most widely used web framework in Go, sitting as a thin layer on top of
net/http - A server works with just the standard library, but Gin reduces the repetition of JSON responses, validation, and common processing
- Install with
go get github.com/gin-gonic/gin; the series is based on Gin v1.10 - Create a router with
gin.Default(), register handlers withr.GET, and start withr.Run() - A response is a single line:
c.JSON(http.StatusOK, gin.H{...}) - Use standard constants like
http.StatusOKfor status codes - For hot reload during development, air is convenient
In the next post (#2 Routing and handlers), we’ll cover how to read path parameters and query strings, and how to group endpoints with router groups.