go1.27: generic methods

A method in Go is a function attached to a type. A generic method is a method that declares its own type parameters, allowing it to work with types that are not fixed by its receiver.

Consider a generic list:

type List[E any] []E

E represents the type of the elements stored in the list. Say:

List[int]
List[string]

Now suppose we want a method that transforms every element:

func (list List[E]) Map[R any](transform func(E) R) List[R] {
result := make(List[R], len(list))

for i, element := range list {
result[i] = transform(element)
}

return result
}

This method uses two type parameters:

– E belongs to the receiver, List[E].
– R belongs to the method itself and represents the result type.

That distinction is important. A List[int] already determines that E is int, but each call to Map may produce a different type:

numbers := List[int]{1, 2, 3}

strings := numbers.Map(func(number int) string {
return strconv.Itoa(number)
})

fmt.Println(strings) // [1 2 3]

Here, the compiler infers that R is string from the transformation function. Conceptually, the method converts:

List[int] > List[string]

A regular method on a generic type can use only the type parameters declared by its receiver:

func (list List[E]) First() E {
return list[0]
}

A generic method goes further by introducing additional type parameters of its own:

func (list List[E]) Map[R any](...) List[R]

This makes generic methods particularly useful for operations such as mapping, converting, decoding, or wrapping values—any operation whose output type may differ from the receiver’s existing type.

Generic methods follow the same central idea as generic functions: write the behavior once while preserving compile-time type safety. The difference is simply that the behavior belongs to a type and can be called using ordinary method syntax.

This syntax is specified in Go 1.27’s section on method declarations(https://go.dev/ref/spec#Method_declarations.

JWT vs Basic Auth

Key Differences:

– Basic Auth
– JWT

1. Credentials sent
– Username/password sent with every request (Base64 encoded)
– Credentials sent once; subsequent requests use token

2. Server validation
– Database query needed for every request
– Can validate cryptographically without database lookup

3. Stateless
– Stateful (server validates against stored credentials)
– Stateless (verification is self-contained)

4. Token expiration
– Credentials always valid until password changes
– Tokens expire automatically

5. Scalability
– Each request hits the database/authentication service
– No per-request database dependency

6. Credentials exposure
– Username/password sent repeatedly (more risk)
– Credentials stay on client; only token travels

7. Revocation
– Immediate (disable account or change password)
– Difficult without a blacklist or waiting for expiration

8. Mobile-friendly
– Poor (storing credentials securely is hard)
– Better (store token, send with requests)

JWT is better when you need:
1. To reduce Server Load: with Basic auth, the server must validate credentials on every single request, typically hitting a database. With JWT, the server verifies the signature cryptographically without needing to look up user data
2. Stateless Architecture: JWT enables truly stateless servers. You can scale horizontally without sharing session data across servers. Multiple servers can all verify the same JWT independently using the same secret key.
3. Run mobile & microservices: with JWT, mobile apps can store a token locally (not credentials), send it with every API call and tokens can be issued for different services with different permissions. Basic auth requires storing credentials on the device, which is a security risk.
4. Send credentials once: with Basic auth, you’re sending username/password (encoded, not encrypted) on every request. With JWT, credentials are sent only at login; the token is sent afterward.

Basic auth is preferable when:
1. Building a simple internal tool with few users
2. You need immediate revocation (can’t wait for token expiration)
3. You’re developing a small monolithic server without scalability concerns
4. Credentials are rarely used (e.g., API management endpoints)

Working example of JWT authentication over gin:

package main

import (
"net/http"
"strings"
"time"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)

// --- Config

var jwtSecret = []byte("super-secret-key-change-me-in-prod")

// Hardcoded users
var users = map[string]string{
"alberto": "password123",
"admin": "admin",
}

// --- JWT helpers

type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}

func generateToken(username string) (string, error) {
claims := Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "jwt-demo",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}

func parseToken(raw string) (*Claims, error) {
token, err := jwt.ParseWithClaims(raw, &Claims{}, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return jwtSecret, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, jwt.ErrTokenInvalidClaims
}
return claims, nil
}

// --- Middleware

func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "missing or malformed Authorization header",
})
return
}

raw := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := parseToken(raw)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "invalid or expired token",
})
return
}

// Stash the username for downstream handlers.
c.Set("username", claims.Username)
c.Next()
}
}

// --- Handlers

type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}

// POST /auth
func handleAuth(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "username and password are required"})
return
}

expected, ok := users[req.Username]
if !ok || expected != req.Password {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}

token, err := generateToken(req.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not generate token"})
return
}

c.JSON(http.StatusOK, gin.H{
"token": token,
"expires_in": "1h",
})
}

// GET /secret_data
func handleSecretData(c *gin.Context) {
username, _ := c.Get("username")
c.JSON(http.StatusOK, gin.H{
"message": "Welcome to the secret vault!",
"user": username,
"secrets": []string{
"secret one",
"secret two",
"secret three",
},
})
}

// --- Main

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

r.POST("/auth", handleAuth)

protected := r.Group("/")
protected.Use(authMiddleware())
{
protected.GET("/secret_data", handleSecretData)
}

r.Run(":8080") // listen on http://localhost:8080
}
JWT Auth

– client auth:

curl -X POST http://localhost:8080/auth -H "Content-Type: application/json"  -d '{"username":"alberto", "password":"password123"}'
client auth

– client fetch:

TOKEN="<...>"
curl http://localhost:8080/secret_data -H "Authorization: Bearer $TOKEN"
client fetch

AdP