Go’s built-in net/http package provides everything you need to create a basic web server, handle HTTP requests, and build simple RESTful APIs. But as your application grows, managing routes, middleware, and HTTP methods can become more complex. This is where third-party frameworks like Gin come into play, offering advanced features and cleaner code for handling these complexities.

This story is free for everyone: Free Link
Part 6: Link
Part 8: Link

The net/http Package in Go

The net/http package is part of Go's standard library and provides all the necessary functionality for creating HTTP servers and clients. It helps handle HTTP requests and responses, as well as routing.

  • HTTP Request: Contains the client-side request details (URL, headers, body, etc.).
  • HTTP Response: Contains the server-side response details (status code, headers, body, etc.).

Creating a Basic HTTP Server

In Go, creating an HTTP server is straightforward. The simplest way to do this is to use http.ListenAndServe() along with http.HandleFunc() for routing.

package main

import (
"fmt"
"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, Go Web Server!")
}

func main() {
// Handle the root URL
http.HandleFunc("/", handler)

// Start the server on port 8080
fmt.Println("Starting server on :8080...")
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server:", err)
}
}
  • http.HandleFunc("/path", handler): This function maps the URL path / to the handler function. This means whenever a request is made to /, the handler will be invoked.
  • http.ListenAndServe(":8080", nil): Starts the server on port 8080.

How to run this example:

  1. Save the code to a file, for example main.go.
  2. Run go run main.go.
  3. Open http://localhost:8080/ in a browser, and you should see "Hello, Go Web Server!".

You can map multiple routes to different functions to handle various paths in your application.

Handling Different HTTP Methods (GET, POST, PUT, DELETE)

In web development, it’s common to handle different HTTP methods for the same route, each corresponding to a different action (e.g., fetching data, creating data, updating data, or deleting data).

package main

import (
"fmt"
"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
fmt.Fprintf(w, "GET Request received. Here's your data.")
case http.MethodPost:
fmt.Fprintf(w, "POST Request received. Data is being created.")
case http.MethodPut:
fmt.Fprintf(w, "PUT Request received. Data is being updated.")
case http.MethodDelete:
fmt.Fprintf(w, "DELETE Request received. Data is being deleted.")
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}

func main() {
http.HandleFunc("/api", handler) // Handle /api path

fmt.Println("Starting server on :8080...")
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server:", err)
}
}
  • http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete are constants that represent the respective HTTP methods.
  • The switch statement inside the handler function checks the request method (r.Method) and responds accordingly.
  • If an unsupported method is used, we send a “Method Not Allowed” response using http.Error().

Use tools like Postman or cURL to send GET, POST, PUT, and DELETE requests to http://localhost:8080/api.

Handling Query Parameters

HTTP requests can include query parameters (e.g., ?name=value). These can be accessed using r.URL.Query().

package main

import (
"fmt"
"net/http"
)

func greetHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name") // Get query parameter 'name'
if name == "" {
name = "Guest"
}
fmt.Fprintf(w, "Hello, %s!", name)
}

func main() {
http.HandleFunc("/greet", greetHandler)

fmt.Println("Starting server on :8080...")
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server:", err)
}
}
  • The query parameter name is fetched using r.URL.Query().Get("name").
  • If no name is provided, the server defaults to "Guest".

Example request:
http://localhost:8080/greet?name=Xwould output "Hello, X!"

The net/http package in Go is great for basic web server functionality, but as your application grows, it can become hard to manage complex routing, middleware, and features like dynamic URL variables.

There are several reasons why it’s recommended to avoid using the above approach(and why it’s a good idea to create your own ServeMux).

What is Mux?

In electronics, a “multiplexer” is a tool that takes multiple input signals and routes them to one output channel. Similarly, a “mux” in web servers takes incoming HTTP requests (input) and routes them to the appropriate handler (output).

In Go, http.ServeMux is used through http.HandleFunc() and http.ListenAndServe(). It matches the incoming request URL with predefined URL patterns (like /home, /api/{id}, etc.) and directs them to corresponding handler functions.

ServeMux in Go:

package main

import (
"fmt"
"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from the default mux!")
}

func main() {
mux := http.NewServeMux() // Create a new ServeMux instance

// Define routes
mux.HandleFunc("/", handler) // Handle GET requests for "/"
mux.HandleFunc("/greet", greetHandler) // Handle GET requests for "/greet"

// Start the server
fmt.Println("Starting server on :8080...")
err := http.ListenAndServe(":8080", mux)
if err != nil {
fmt.Println("Error starting server:", err)
}
}

func greetHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from /greet route!")
}

What is the Default Mux in Go?

In Go, there is a default ServeMux is a globally available, which is used when you call http.HandleFunc() and http.ListenAndServe() like in the previous approach. When you use http.HandleFunc(), Go automatically registers handlers with this global ServeMux instance.

The default mux is global, which means it’s shared across the entire application. If you modify it in one part of your program, it can have unintended side effects elsewhere.

If you register a route globally (with http.HandleFunc()), it’s available everywhere in your program, including parts you might not expect.

If you have multiple web servers or need to register routes in a modular way, using the global mux can lead to clashes or unintentional overrides.

For production applications, it is generally advised to use a third party mux like gorilla or gin, for various reasons we will see later.

Structuring Routes and Handlers for RESTful APIs

When building RESTful APIs in Go, it’s important to structure your routes and handlers in a way that is both scalable and maintainable. Here’s a general approach to structure your routes and handlers:

Key Principles of RESTful API Design:

  • Resources: Each route corresponds to a resource (e.g., /users, /products).

HTTP Methods: Use appropriate HTTP methods to represent actions:

  • GET: Retrieve data.
  • POST: Create new data.
  • PUT/PATCH: Update data.
  • DELETE: Delete data.

Status Codes: Use the right HTTP status codes to represent the success or failure of a request (e.g., 200 OK, 201 Created, 400 Bad Request, 404 Not Found).

Example of Structuring Routes for a RESTful API:

Let’s say we’re building a simple API for managing users. Here’s how we can structure the routes:

  • GET /users: List all users.
  • GET /users/{id}: Get a specific user by ID.
  • POST /users: Create a new user.
  • PUT /users/{id}: Update a user’s details.
  • DELETE /users/{id}: Delete a user by ID

Working with JSON: The encoding/json Package

In a RESTful API, it’s common to use JSON to send and receive data. Go provides the encoding/json package to handle JSON encoding and decoding.

JSON Encoding (Request Body)

For incoming data (such as in a POST or PUT request), you need to decode the JSON from the request body. The json.NewDecoder() function can be used to achieve this.

var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}

Here, we decode the incoming JSON into a User struct. If the request body is not valid JSON, an error is returned.

JSON Encoding (Response Body)

For outgoing data, you use json.NewEncoder() to encode the response into JSON format:

json.NewEncoder(w).Encode(users)

This encodes the users slice into a JSON response, which is then sent back to the client.

Working with JSON Struct Tags

Go uses struct tags to control how fields in a struct are serialized into JSON and vice versa. For example:

type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

The json:"id" tag tells Go that the ID field should be named id in the JSON representation.

Middleware in Go Web Applications

In Go, middleware refers to functions that sit between the incoming HTTP request and the final handler that processes the request. Middleware is often used for tasks such as logging, authentication, and error handling before the request reaches the handler. Middleware can modify the request or response, or it can perform operations before passing control to the next middleware or the final handler.

What is Middleware?

Middleware is a layer of code that intercepts the HTTP request or response. It allows you to execute code before or after the request is handled. Middleware can perform a wide range of tasks such as:

  • Logging requests and responses
  • Authenticating users
  • Enabling CORS (Cross-Origin Resource Sharing)
  • Handling errors
  • Validating inputs
  • Modifying the request or response
  • Performing any other pre- or post-processing logic

Middleware can be applied globally (to all routes) or to specific routes, giving you flexibility in how you control the behavior of your web application.

How to Build Simple Middleware in Go

Middleware is simply a function that takes an http.Handler as an argument and returns a new http.Handler. The new handler can either call the original handler or modify the request/response before passing control.

Here’s the basic structure for a simple logging middleware that logs incoming requests:

package main

import (
"fmt"
"net/http"
"time"
)

// Logging middleware
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
fmt.Printf("Started %s %s\n", r.Method, r.URL.Path)

// Call the next handler
next.ServeHTTP(w, r)

duration := time.Since(start)
fmt.Printf("Completed %s %s in %v\n", r.Method, r.URL.Path, duration)
})
}

func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, Go Web Server!")
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)

// Wrap the mux with logging middleware
http.ListenAndServe(":8080", LoggingMiddleware(mux))
}

LoggingMiddleware logs the method and path of the incoming request, then records the duration of the request processing.It wraps the default HTTP handler (mux) to log the requests.

When you send a request, the log will show something like:

Started GET /
Completed GET / in 4.216ms

Third-Party Web Frameworks in Go

While the net/http package in Go's standard library provides all the necessary functionality to build web applications, third-party web frameworks offer additional features that make it easier to build more complex web applications quickly. These frameworks are designed to improve productivity by simplifying routing, request handling, and middleware management, among other things.

Four popular Go web frameworks are Gin, Gorilla Mux, Echo, and Chi. Each has its strengths, and the choice of framework depends on the needs of your preference and application.

Let’s dive deeper into Gin, which is one of the most widely used web frameworks in Go.

Gin Web Framework

Gin is a high-performance web framework designed for building web APIs. It is known for its speed and simplicity. Gin provides a powerful set of features like middleware support, request routing, and JSON handling, but it’s designed to be lightweight and fast.

Key Features of Gin

  1. Fast and Lightweight: Gin is designed to be extremely fast while maintaining ease of use. It’s considered one of the fastest web frameworks for Go.
  2. Middleware Support: Just like the net/http package, Gin supports middleware, but it provides additional utilities for easier middleware chaining.
  3. JSON Handling: Gin simplifies working with JSON data, both for incoming requests and outgoing responses, making it a great choice for building RESTful APIs.
  4. Routing: Gin has a highly optimized router that supports variables, wildcards, and grouping, making it flexible for handling complex routes.
  5. Error Handling: Gin provides a streamlined way to handle errors and return consistent error responses in the correct format.
  6. HTML Rendering: While not as feature-rich as some other frameworks, Gin can render HTML templates with a built-in view engine.
  7. Built-in Validation: Gin supports built-in request validation (e.g., validating request parameters, query parameters, JSON payload) via the binding mechanism.
  8. Group Routes: You can group similar routes under a common path, which can make your code more modular and maintainable.
package main

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

// Define a route handler for GET /hello
func helloHandler(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, Gin!",
})
}

// Define a POST handler for /user
func createUserHandler(c *gin.Context) {
var user map[string]string
if err := c.BindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": "Invalid input"})
return
}
c.JSON(201, gin.H{"status": "User created", "user": user})
}

func main() {
// Create a new Gin router
r := gin.Default()

// Define routes
r.GET("/hello", helloHandler)
r.POST("/user", createUserHandler)

// Start the server
r.Run(":8080")
}

Gin Router: r := gin.Default() creates a Gin router instance with default middleware (like logging and recovery).

  • GET /hello: When a GET request is made to /hello, it responds with a JSON message.
  • POST /user: A POST request with JSON data to /user will be parsed and the response will include the created user.

Gin Middleware Example

Middleware in Gin is easy to implement, just like in net/http. Here’s how to add logging middleware in Gin:

func loggingMiddleware(c *gin.Context) {
fmt.Println("Request received:", c.Request.Method, c.Request.URL)
c.Next() // Pass control to the next handler
}

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

// Add the logging middleware globally
r.Use(loggingMiddleware)

r.GET("/hello", helloHandler)
r.Run(":8080")
}

There are plenty of other middlewares for session management, authentication etc. pre-built for Gin which you can find here.

Differences Between Using Gin and net/http

While the net/http package can handle HTTP requests and responses, frameworks like Gin provide higher-level abstractions that are specifically designed for web development.

Here are some key differences:

1. Routing

  • net/http: You need to manually route requests using http.HandleFunc() or http.ServeMux. It’s a basic approach with no grouping, path variables, or middleware integration.
  • Gin: Provides a powerful routing mechanism that includes path variables, route grouping, and route parameters. You can also use named routes and set custom middleware for specific routes or groups.

2. Middleware

  • net/http: Middleware is not directly supported. You would need to manually chain middleware or wrap the http.Handler to create a custom middleware stack.
  • Gin: Built-in support for middleware. Gin makes it easy to add middleware globally or for specific routes, with minimal boilerplate code.

3. JSON Handling

  • net/http: You need to manually parse and format JSON requests and responses using the encoding/json package.
  • Gin: Provides easy-to-use JSON parsing and rendering. You can bind JSON data directly to Go structs with c.ShouldBindJSON(), making it much simpler for APIs that handle JSON.

4. Performance

  • net/http: As part of the standard library, net/http is fast, but Gin is specifically optimized for performance and has a fast router.
  • Gin: In benchmarks, Gin is one of the fastest frameworks for building web applications in Go. It’s designed to be lightweight and efficient.

5. Error Handling

  • net/http: Error handling is more manual. You need to check for errors and send appropriate responses using http.Error().
  • Gin: Has built-in error handling and allows you to return consistent error responses with JSON and custom error codes.

Key Takeaway:

In this part of the crash course, we’ve covered the essentials of web development in Go, starting with the net/http package and using third-party frameworks like Gin for building fast and scalable web applications.

Explore the Gin, Gorilla, Echo, or Chi frameworks further by building full-fledged RESTful APIs, adding more sophisticated middleware, and implementing authentication and authorization.

For the next part of the Go crash course, where we’ll explore advanced topics like websockets, database integration, and deploying Go applications to production!