7 lessons, about 29 minutes. The check's questions come from these.
Starting Go services
1. Start a Go module
4 min
Why modules first
Your service starts as a module. A module is the unit Go uses to track your packages, dependencies, and Go version.
For this track, your final repo must be easy for a recruiter to clone and run. A clean go.mod file is the first proof that your project is a real Go project, not loose files.
Key point
Remember this
Start every Go service inside a module, before writing handlers or adding packages. The go.mod file is the project’s dependency record.
Your to-do
Create the project
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
mkdir go-fast-service
cd go-fast-service
go mod init github.com/YOUR_USERNAME/go-fast-service
Example
First runnable file
Keep the first file boring. The goal is to prove the module runs. HTTP routes, types, errors, and concurrency come later in this track.
This program prints one line and exits. That is enough to check your Go install, folder, and module setup.
2. Model data with types
4 min
Why types matter
Your HTTP service will receive, store, and return data. If that data is loose, every handler becomes a mess of string keys and checks.
In Go, you model data with structs, slices, and maps. This makes your repo easier for a recruiter to read and easier for you to test.
Key point
One idea
Create types for the real things in your app. Then pass those types through your handlers instead of passing raw maps everywhere.
Example
Model a project
For a HackIndia-style backend, you may track student projects. Start with a struct that names the data clearly.
JSON tags control the field names your API sends and receives. Keep Go field names exported with capital letters if another package or JSON encoder must access them.
package main
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Team string `json:"team"`
College string `json:"college"`
Tags []string `json:"tags"`
RepoURL string `json:"repoUrl"`
LiveURL string `json:"liveUrl,omitempty"`
}
type ProjectStore struct {
items map[string]Project
order []string
}
Interfaces and errors
3. Use interfaces by behavior
4 min
Key point
One idea
In Go, a type implements an interface by having the methods the interface asks for. You do not write “implements”.
This means your service can depend on behavior, such as “can find a student”, instead of a concrete detail, such as “uses Postgres”.
Why it matters
Your HackIndia repo needs typed handlers and clean design. Interfaces help you keep HTTP code separate from storage code.
A recruiter should be able to read your handler and see the business action. They should not need to understand your map, file, or database code first.
Your to-do
Design the interface
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
Concurrency API and shipping
5. Add goroutines and channels
4 min
Key point
One idea
A goroutine runs a function at the same time as the rest of your program. A channel lets that goroutine send a typed value back safely.
Use this for work that can happen in the background, like logging, sending a notification, checking a slow dependency, or preparing a report.
Where it fits
Your service does not need complex concurrency to look serious. One clear use is enough for this track.
For a college project, you can record a visit count, run a fake email receipt, or calculate a summary in the background after an HTTP request.
Your to-do
Use the pattern
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
package main
import "fmt"
func main() {
fmt.Println("go-fast-service is running")
}
Your to-do
Run and build
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 4 done
go run .
go build
./go-fast-service
Tip
Pick a stable module path
If your repo will be public, choose the final repo path now, like github.com/yourname/go-fast-service. It makes imports clear later when your project grows into multiple packages.
If you are just testing locally, a temporary path works. But change it before submitting your HackIndia project.
Common mistake
Do not skip go.mod
Beginners sometimes write main.go first and only fix modules after adding dependencies. That causes messy imports and confusing setup steps.
Also remember that go run is not the same as shipping. It compiles and runs during development, but it does not leave you with the final executable. Use go build for that.
Your to-do
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
Your to-do
Build useful shapes
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
Example
Use make correctly
Slices and maps often need initialization before use. The built-in make creates initialized slices, maps, and channels and returns the value, not a pointer.
A nil map will panic when you assign into it. So initialize your store before your handlers use it.
func NewProjectStore() *ProjectStore {
return &ProjectStore{
items: make(map[string]Project),
order: make([]string, 0),
}
}
func (s *ProjectStore) Add(p Project) {
s.items[p.ID] = p
s.order = append(s.order, p.ID)
}
func (s *ProjectStore) All() []Project {
projects := make([]Project, 0, len(s.order))
for _, id := range s.order {
projects = append(projects, s.items[id])
}
return projects
}
Common mistake
Avoid map soup
Do not model a project as map[string]string just because JSON looks like key-value data. That hides mistakes until runtime.
With a struct, the compiler catches p.RepoURL versus p.RepoUrl. With a raw map, a typo becomes bad data in your API response.
Tip
Use AI carefully
You can ask AI to draft structs from a sample JSON response. Then check three things yourself: field names are clear, optional fields use omitempty only when empty is valid, and lists are slices, not comma-separated strings.
If AI gives you interface{} for normal app data, push back. Ask for concrete structs unless the field can truly hold many unrelated shapes.
Your to-do
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
0 of 5 done
Example
Small store example
Here the handler depends on StudentStore. The mapStore type matches it because it has FindByID with the same signature.
No extra keyword is needed. If the method is missing or has the wrong return type, the code will not compile.
package main
import "fmt"
type Student struct {
ID string
Name string
}
type StudentStore interface {
FindByID(id string) (Student, bool)
}
type mapStore struct {
students map[string]Student
}
func (m mapStore) FindByID(id string) (Student, bool) {
student, ok := m.students[id]
return student, ok
}
func PrintStudent(store StudentStore, id string) {
student, ok := store.FindByID(id)
if !ok {
fmt.Println("student not found")
return
}
fmt.Println(student.Name)
}
func main() {
store := mapStore{
students: map[string]Student{
"101": {ID: "101", Name: "Asha"},
},
}
PrintStudent(store, "101")
}
Tip
Use fakes
Interfaces make testing easier. You can pass a fake store to a handler instead of connecting to a real database or editing live data.
For a college project, this is enough to show good backend thinking. Your README can say the HTTP layer is tested with a fake store.
Do not use interface{} because you are unsure of the type. It means any non-interface type can fit, so you lose useful compiler checks.
Prefer concrete types or a small interface with named methods. Your service will be easier to read and safer to change.
Go 1.27 note
Go 1.27 supports generic methods, so older tutorials saying Go has no generic methods are outdated.
But interface methods may not declare type parameters, and generic methods cannot implement interface methods. Keep service interfaces simple and non-generic.
Your to-do
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
4. Return useful errors
4 min
Key point
One idea
In Go, useful functions return an error as the last value. nil means success. Any other error means the caller must decide what to do next.
Do not make the caller read a string to understand a known case. Use error values or custom error types for cases your service can handle.
Why this matters
Your HTTP service will fail in normal ways. A product ID may be missing. A map lookup may fail. A request body may be invalid.
Good errors make these failures easy to turn into proper HTTP responses later. Bad errors become random 500s and confusing logs during demos.
Your to-do
Shape errors well
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
Example
Repository error
This example uses a map from your data lesson. The repository returns a typed value and a useful error.
The caller can check ErrProductNotFound without comparing text like "not found".
package store
import (
"errors"
"fmt"
)
type Product struct {
ID string
Name string
Price int
}
var ErrProductNotFound = errors.New("product not found")
type ProductStore struct {
items map[string]Product
}
func NewProductStore() ProductStore {
return ProductStore{
items: map[string]Product{
"chai": {ID: "chai", Name: "Masala Chai", Price: 20},
},
}
}
func (s ProductStore) Get(id string) (Product, error) {
p, ok := s.items[id]
if !ok {
return Product{}, fmt.Errorf("get product %q: %w", id, ErrProductNotFound)
}
return p, nil
}
Tip
Wrap, then check
Use fmt.Errorf with %w when you want to keep the original error inside a clearer message. This gives logs context and still lets code detect the real error.
Later, your HTTP handler can use errors.Is(err, ErrProductNotFound) and return 404 instead of 500.
if errors.Is(err, store.ErrProductNotFound) {
// return HTTP 404 later
}
Common mistake
Avoid string checks
Do not write code that checks err.Error() == "product not found". That breaks when you improve the message.
Also avoid panic for expected user mistakes. Missing input, unknown IDs, and invalid values should return errors, not crash your service.
Example
Validate input
Validation is also a good place for useful errors. Keep the message short, but mention the field that failed.
For prices in rupees, reject zero or negative values before saving.
package store
import "fmt"
func ValidateProduct(p Product) error {
if p.ID == "" {
return fmt.Errorf("id is required")
}
if p.Price <= 0 {
return fmt.Errorf("price must be positive: got %d", p.Price)
}
return nil
}
Your to-do
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
0 of 5 done
Example
Small worker example
This example calculates a total in a goroutine and sends it back on a channel. It uses only the standard library.
A channel made with make(chan int) is unbuffered. The send waits until another goroutine receives the value.
package main
import "fmt"
type Order struct {
ID string
Amount int
}
func totalAsync(orders []Order) <-chan int {
result := make(chan int)
go func() {
total := 0
for _, order := range orders {
total += order.Amount
}
result <- total
}()
return result
}
func main() {
orders := []Order{
{ID: "upi-101", Amount: 199},
{ID: "upi-102", Amount: 249},
}
totalCh := totalAsync(orders)
fmt.Println("doing other work")
total := <-totalCh
fmt.Println("total rupees:", total)
}
Tip
Use direction
Return a receive-only channel when callers should only read from it. That is why the example returns <-chan int.
This makes your intent clear and prevents accidental sends from the wrong place.
Common mistake
Avoid goroutine leaks
Do not start a goroutine that sends to a channel if nobody will receive from it. It can wait forever.
Also do not add goroutines just to look advanced. If normal code is clearer, use normal code.
Example
Buffered channel option
A channel made with make(chan string, 1) has space for one value. The sender can place one value without waiting for the receiver immediately.
This is useful for small one-shot signals, not for hiding slow code.
func sendReceiptAsync(orderID string) <-chan string {
done := make(chan string, 1)
go func() {
// Replace this later with real email, SMS, or webhook code.
done <- "receipt queued for " + orderID
}()
return done
}
Your to-do
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
6. Build the HTTP API
5 min
Key point
One job per handler
An HTTP handler should translate web input into your Go types, call your service logic, then translate the result back to HTTP.
Keep business rules out of the handler. This makes your API easier to test and easier for a recruiter to read.
Use net/http
Go ships with an HTTP server in the standard library. You can register a route with http.HandleFunc and start the server with http.ListenAndServe.
:8080 means your service listens on port 8080 on any interface. ListenAndServe blocks, so your program keeps running until you stop it.
Your to-do
Shape the endpoint
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
Example
Minimal API
This example stores tasks in memory. It shows typed handlers, maps, JSON, and method checks. Use this pattern with the types you created earlier in the track.
For Go 1.27, encoding/json keeps the familiar API while using the newer implementation behind it, so this code stays simple.
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
)
type Task struct {
ID int `json:"id"`
Text string `json:"text"`
Done bool `json:"done"`
}
type CreateTaskRequest struct {
Text string `json:"text"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
var tasks = map[int]Task{}
var nextID = 1
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, ErrorResponse{Error: msg})
}
func tasksHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
list := make([]Task, 0, len(tasks))
for _, task := range tasks {
list = append(list, task)
}
writeJSON(w, http.StatusOK, list)
case http.MethodPost:
var req CreateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
req.Text = strings.TrimSpace(req.Text)
if req.Text == "" {
writeError(w, http.StatusBadRequest, "text is required")
return
}
task := Task{ID: nextID, Text: req.Text}
tasks[nextID] = task
nextID++
writeJSON(w, http.StatusCreated, task)
default:
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func taskByIDHandler(w http.ResponseWriter, r *http.Request) {
idText := strings.TrimPrefix(r.URL.Path, "/tasks/")
id, err := strconv.Atoi(idText)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid task id")
return
}
task, ok := tasks[id]
if !ok {
writeError(w, http.StatusNotFound, "task not found")
return
}
writeJSON(w, http.StatusOK, task)
}
func main() {
http.HandleFunc("/tasks", tasksHandler)
http.HandleFunc("/tasks/", taskByIDHandler)
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Tip
Test with curl
Keep sample curl commands in your README. They prove your service runs without needing Postman or a frontend.
Run the server in one terminal. Use curl from another terminal.
go run .
curl http://localhost:8080/tasks
curl -X POST http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"text":"pay mess bill by UPI"}'
curl http://localhost:8080/tasks/1
Common mistake
Do not hide errors
A common beginner mistake is returning plain text errors sometimes and JSON errors other times. Pick one style for your API.
Also, do not ignore json.NewDecoder errors. Bad input should get a 400 response, not a fake empty task.
Your to-do
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
7. Ship one binary repo
4 min
Key point
One thing matters
Your repo should prove this: a recruiter can clone it, run one command to build it, then start one binary.
go run is fine while coding, but it does not create the executable you submit. Use go build for the shippable path.
What to ship
A strong final repo is boring in a good way. It has a Go 1.27 module, a clear main package, typed handlers, useful errors, slices or maps, one goroutine or channel use, and proof that the API works.
Keep the shape simple. Do not hide your service behind too many folders just to look senior.
Example
Good repo shape
This is enough for a small internship-ready backend project. Adjust names for your idea, like mess-menu-api, upi-splitter, or placement-tracker.
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
go mod tidy
go build
./mess-menu-api
Example
README that works
Your README should answer four questions fast: what it is, how to run it, how to build it, and how to test one endpoint.
Use commands that you have actually run on your laptop. If AI writes your README, check every command before committing.
# Mess Menu API
Small Go 1.27 HTTP service for storing hostel mess menu items.
## Requirements
Go 1.27 or later
## Run during development
go run .
The service listens on :8080.
## Build one binary
go mod tidy
go build
./mess-menu-api
## Try it
curl http://localhost:8080/health
curl http://localhost:8080/menu
Tip
Use a Makefile
A Makefile gives reviewers short commands without making them learn your folder structure. Keep it tiny.
If you are on Windows without make, the README commands still matter. The Makefile is a helper, not the only way to run the project.
run:
go run .
build:
go mod tidy
go build
test-api:
curl http://localhost:8080/health
curl http://localhost:8080/menu
Common mistake
Common shipping mistakes
Do not submit only screenshots or only a deployed URL. The task is a public repository someone can inspect and run.
Do not leave your README saying go run if your project needs environment variables, a port, or seed data. Write the real steps. Also avoid copying old Go advice blindly; Go 1.27 modules may tidy require blocks differently than old tutorials show.
Your to-do
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.