Skip to content

New to Go? Read This First

Version: 1.0 | Written 2026-09-16

You don't know Go yet — this page is the minimum vocabulary to follow the backend lessons without stalling on syntax. Not a full tutorial. Skim it once, then come back whenever a lesson uses a word from here.

The shape of a Go file

go
package services

import (
    "context"
    "github.com/medyzen-health/medyzen-backend/internal/order/models"
)

type OrderService struct {
    repo Repository
}

func (s *OrderService) GetByID(ctx context.Context, id int64) (*models.Order, error) {
    return s.repo.FindByID(ctx, id)
}
  • package — every file belongs to one, matching its folder. internal/order/services/*.go is all package services.
  • import — other packages this file uses, by their full module path.
  • type X struct { ... } — a data shape, like a class with only fields, no methods attached directly.
  • func (s *OrderService) GetByID(...) — a method. The (s *OrderService) part is the "receiver": this function is attached to OrderService. Read it as "OrderService has a method called GetByID".
  • (*models.Order, error) — Go functions that can fail return two values: the result, and an error. There are no exceptions to catch — every call you make, you check the second value.

Reading a function signature

go
func CreateOrder(ctx context.Context, patientID int64, items []CartItem) (*Order, error)

Left to right: name, then each parameter as name Type, then what it returns in parentheses. []CartItem means "a slice (list) of CartItem". *Order means "a pointer to an Order" — see below.

context.Context — you'll see it everywhere

Almost every function that does I/O (database, HTTP, Redis) takes a ctx context.Context as its first argument. It carries two things: a deadline/cancellation signal (if the caller gives up or times out, everything downstream stops), and small pieces of request-scoped data (like the logged-in user, via internal/pkg/ctxkeys). You don't need to understand its internals — just always pass the ctx you were given down to the next call, don't create a new empty one.

Pointers (*Order vs Order)

Order is a value — passing it copies the whole struct. *Order is a pointer — passing it shares the same struct in memory. Two rules that cover 90% of what you'll read:

  • A method with receiver (s *OrderService) can modify s's fields; (s OrderService) gets its own copy and any change is thrown away when the method returns.
  • A function returning (*Order, error) returns nil, err on failure — nil is Go's "no pointer here", checked before you touch the result: if err != nil { return err } first, then use the order.

Errors are values, not exceptions

go
order, err := repo.FindByID(ctx, id)
if err != nil {
    return nil, err
}

There's no try/catch. Every call that can fail returns an error as its last value, and the very next lines almost always check it. When you read a long function, the if err != nil blocks are the failure branches — skip past them on a first read to see the happy path, then re-read for what can go wrong.

Goroutines and channels (background workers)

go someFunc() starts someFunc running concurrently — it doesn't block the caller. The backend's background workers (Day 5) are started this way at boot and keep running for the life of the process. A chan (channel) is how goroutines signal each other — commonly you'll see done := make(chan struct{}) and later close(done), which is a goroutine saying "I've finished" to whatever is waiting with <-done.

Structs, interfaces, and "handler → service → repository"

  • A struct is data: type Order struct { ID int64; Status string }.
  • An interface is a contract: a list of method signatures with no implementation. type Repository interface { FindByID(ctx, id) (*Order, error) }. Any struct that has those methods satisfies the interface automatically — no implements keyword.
  • The backend's layering (Day 1) uses interfaces so a service depends on a Repository interface, not a concrete database struct — a test can swap in a fake that satisfies the same interface.

GORM (the database library)

GORM maps Go structs to Postgres tables. db.Where("id = ?", id).First(&order) reads roughly as SQL. The ? is a placeholder — GORM fills it in safely, which is why raw string concatenation into a query is the thing to avoid (see the SQL-injection trap in Day 2).

What you can skip for now

Generics ([T any]), Go's build tags, and reflection show up in a handful of places (tests, the dev-tagged build) but are not needed to follow the request-handling code. If a lesson hits one, it says so.

A five-minute glossary

TermMeans
nilGo's null/empty — for pointers, slices, maps, interfaces, channels, errors
slice []TA growable list of T
map map[K]VA dictionary, keyed by K
deferRuns a statement right before the function returns, in reverse order — used for cleanup like closing a file
panic / recoverGo's rare, exceptional-only crash + catch; the middleware chain's Recovery catches panics so one bad request doesn't kill the process
struct{}{}An empty struct — zero memory, used as a channel's "just a signal" type

Internal — written against the code, not the other way around.