Context

Context adalah salah satu konkuresi pattern yang bertujuan untuk mengcancel jika menemui sebuah routine yang waktu eksekusinya lama. Karena operasi yang berjalan lama memang seharusnya diberi deadline. Jalan untuk meng-handle pembatalan adalah dengan melempar context.Context to fungsi yang mengetahui proses untuk mengecek pembatalan terminasi dini.

  • Tambahkan argumen context.Context ke semua fungsi, taruh sebagi argumen pertama, misalnya List() ([]model.User, error) diubah menjadi List(ctx context.Context) ([]model.User, error)

  • Passing ctx variable ke db.QueryContext, db.QueryRowContext, db.PrepareContext and stmt.ExecContext di repository (file internal/repository/user_repository.go)

  • Di setiap fungsi yang sekiranya mengambil waktu lama, seperti heavy io, tambahkan select case untuk membaca apakah context sudah berakhir.

    select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}
  • Fungsi yang memerlukan select dengan ctx.Done() adalah :

    1. Operasi I/O yang lama (database query, HTTP request, file I/O)

    2. Operasi yang memanggil external service (API call, RPC)

    3. Fungsi dengan goroutine/channel (waiting for result)

    4. Operasi yang bisa di-cancel/timed out

  • Dalam contoh fitur saat ini, kita hanya membuat simple CRUD yang mana tidak perlu dihandle melalui select dengan ctx.Done(). Potongan kode di atas hanya contoh saja, dan tidak akan diguankan dalam project ini. Namun sebagai antisipasi jika suatu saat kita menumkan case yang membutuhkan handling Select context, kita oerlu menambahkan bisnis error kita dengan Gateway Timeout, mengingat error context ini paling tepat mengembalikan response 504 gateway Timeout.

  • Semua context.Background() dalam pemanggilan log, diubah untuk meneruskan context yang dikirim dari argumen, misalnya u.log.Error(context.Background(), "error: querying users", slog.Any("error", err)) diubah menjadi u.log.Error(ctx, "error: querying users", slog.Any("error", err))

  • Ubah file internal/repository/user_repository.go untuk mengimpmentasikan context

package repository

import (
	"context"
	"database/sql"
	"log/slog"
	"workshop/internal/model"

	"github.com/jacky-htg/go-libs/logger"
)

type UserRepository interface {
	List(ctx context.Context) ([]model.User, error)
	Create(ctx context.Context, user *model.User) error
	FindById(ctx context.Context, id string) (*model.User, error)
	Update(ctx context.Context, user *model.User) error
	Delete(ctx context.Context, id string) error
}

type userRepository struct {
	db  *sql.DB
	log logger.Logger
}

func NewUserRepository(db *sql.DB, log logger.Logger) UserRepository {
	return &userRepository{db: db, log: log}
}

// List : http handler for returning list of users
func (u *userRepository) List(ctx context.Context) ([]model.User, error) {
	query := `SELECT id, name, username, password, email, is_active FROM users WHERE deleted_at IS NULL`
	rows, err := u.db.QueryContext(ctx, query)
	if err != nil {
		u.log.Error(ctx, "error: querying users", slog.Any("error", err))
		return nil, err
	}
	defer rows.Close()

	var users []model.User
	for rows.Next() {
		var user model.User
		if err := rows.Scan(&user.ID, &user.Name, &user.Username, &user.Password, &user.Email, &user.IsActive); err != nil {

			u.log.Error(ctx, "error: scanning user row", slog.Any("error", err))
			return nil, err
		}
		users = append(users, user)
	}

	if err := rows.Err(); err != nil {
		u.log.Error(ctx, "error: iterating user rows", slog.Any("error", err))
		return nil, err
	}

	return users, nil
}

func (u *userRepository) Create(ctx context.Context, user *model.User) error {
	query := `INSERT INTO users (id, name, username, password, email, is_active) VALUES ($1, $2, $3, $4, $5, $6)`
	_, err := u.db.ExecContext(ctx, query, user.ID, user.Name, user.Username, user.Password, user.Email, user.IsActive)
	if err != nil {
		u.log.Error(ctx, "error: inserting user", slog.Any("error", err))
		return err
	}

	return nil
}

func (u *userRepository) FindById(ctx context.Context, id string) (*model.User, error) {
	query := `SELECT id, name, username, password, email, is_active FROM users WHERE id = $1 AND deleted_at IS NULL`
	row := u.db.QueryRowContext(ctx, query, id)

	var user model.User
	if err := row.Scan(&user.ID, &user.Name, &user.Username, &user.Password, &user.Email, &user.IsActive); err != nil {
		if err == sql.ErrNoRows {
			return nil, nil
		}
		u.log.Error(ctx, "error: scanning user row", slog.Any("error", err))
		return nil, err
	}

	return &user, nil
}

func (u *userRepository) Update(ctx context.Context, user *model.User) error {
	query := `UPDATE users SET name = $1, is_active = $2 WHERE id = $3 RETURNING username, email`
	err := u.db.QueryRowContext(ctx, query, user.Name, user.IsActive, user.ID).Scan(&user.Username, &user.Email)
	if err != nil {
		u.log.Error(ctx, "error: updating user", slog.Any("error", err))
		return err
	}

	return nil
}

func (u *userRepository) Delete(ctx context.Context, id string) error {
	query := `UPDATE users SET deleted_at = timezone('utc', now()) WHERE id = $1`
	_, err := u.db.ExecContext(ctx, query, id)
	if err != nil {
		u.log.Error(ctx, "error: deleting user", slog.Any("error", err))
		return err
	}

	return nil
}
  • Ubah file internal/service/users.go untuk mengimplmentasikan contaxt

  • Ubah file internal/handler/user_handler.go untuk mengimpmentasikan contaxt

  • Ubah file pkg/response/response.go untuk mengimplmentasikan context

  • Ubah file pkg/errors/error.go untuk menambahkan error Gateway Timeout

Last updated