Common Interfaces — Tasks¶
Exercise structure¶
- 🟢 Easy — for beginners
- 🟡 Medium — middle level
- 🔴 Hard — senior level
- 🟣 Expert — professional level
A solution for each exercise is provided at the end.
Easy 🟢¶
Task 1 — Implement fmt.Stringer for an enum¶
type Priority int
const ( Low Priority = iota; Medium; High; Critical )
// Write Priority.String() — fmt.Println(High) → "high"
Task 2 — Implement the error interface¶
Define type NotFoundError struct{ Resource string } so that fmt.Println(&NotFoundError{Resource: "user"}) prints user not found.
Task 3 — Implement io.Reader for a string¶
type StringReader struct { s string; pos int }
// Write Read so that io.ReadAll(&StringReader{s: "hello"}) returns []byte("hello")
Task 4 — Implement io.Writer that counts bytes¶
Task 5 — Implement sort.Interface for a []int¶
Sort a []int in descending order using sort.Sort (not sort.Sort with sort.Reverse). Define a wrapper type.
Medium 🟡¶
Task 6 — Custom MarshalJSON with omit logic¶
// type Profile struct { Name string; Bio string; Email *string }
// MarshalJSON must:
// - always include "name"
// - include "bio" only if Bio != ""
// - include "email" only if Email != nil
Task 7 — Implement encoding.TextMarshaler for a UUID¶
type UUID [16]byte
// MarshalText -> 8-4-4-4-12 hex form ("550e8400-e29b-41d4-a716-446655440000")
// UnmarshalText must parse the same form
Task 8 — io.WriterTo optimization¶
Implement a Buf type whose WriteTo writes its slice in one Write call, and verify with a benchmark that io.Copy(dst, &buf) uses the fast path.
Task 9 — http.Handler that returns JSON¶
Write JSONHandler[T any] that wraps a function func(*http.Request) (T, error) and returns either a JSON body or http.Error.
Task 10 — Context-aware downloader¶
Write Download(ctx context.Context, url string) ([]byte, error) that respects cancellation and a 10s default timeout.
Task 11 — Implement io.ReadCloser for a string¶
A type that satisfies both io.Reader and io.Closer. Close should mark the reader as closed; subsequent Read calls return os.ErrClosed.
Task 12 — sort.Interface with multi-key ordering¶
Sort []Person by LastName then FirstName then Age.
Hard 🔴¶
Task 13 — Custom fmt.Formatter¶
Implement Money with Format(s fmt.State, verb rune) supporting:
%v/%s—"12.50 USD"%d— cents only ("1250")%+v— verbose with tag ("Money{cents=1250 currency=USD}")
Task 14 — Pipe through a transformer¶
Write a gzipWriterTo type whose WriteTo(w io.Writer) reads from an inner io.Reader, gzip-compresses the bytes, and writes to w. io.Copy(dst, gzipWriterTo) should produce gzip output.
Task 15 — Implement sql.Scanner and driver.Valuer¶
// type Roles []string — stored as comma-separated string in the DB
// Implement Scan and Value so it works with sql.DB.QueryRow().Scan(&r)
Task 16 — Streaming HTTP handler with http.Flusher¶
Write /events SSE endpoint that emits a JSON event every second until the client disconnects (use r.Context().Done()). Apply http.Flusher after each write.
Task 17 — Generic iter.Seq filter (Go 1.23+)¶
Write Filter[V any](in iter.Seq[V], pred func(V) bool) iter.Seq[V] that yields only items passing pred. Add a small test with slices.Collect.
Task 18 — Implement io.Seeker for an in-memory blob¶
// type Blob []byte; type BlobCursor struct { b Blob; pos int64 }
// Read, Seek (SeekStart, SeekCurrent, SeekEnd), Close
// Validate negative seeks return a non-nil error
Expert 🟣¶
Task 19 — Implement http.Hijacker-aware WebSocket handshake¶
Write a function that accepts an http.ResponseWriter, type-asserts to http.Hijacker, performs the WebSocket upgrade handshake, and returns the net.Conn. (Skip the framing logic; only the handshake.)
Task 20 — Custom io/fs.FS over a tar archive¶
Define type TarFS struct{ entries map[string][]byte } so that fs.WalkDir and fs.ReadFile work correctly. Implement Open and ReadDir so embedding into a templating layer (e.g. html/template.ParseFS) succeeds.
Task 21 — Composable context.Context chain¶
Implement WithRequestID, WithUser, and WithTrace decorators. Each preserves cancellation semantics of the parent. Verify by canceling the root and observing every child's Done channel close.
Task 22 — Plug-in marshaller registry¶
Build a registry: RegisterMarshaler[T any](fn func(T) ([]byte, error)). At encode time, look up the runtime type and dispatch to the registered function — fall back to json.Marshal. Use reflect.TypeOf for the key.
Solutions¶
Solution 1¶
type Priority int
const (
Low Priority = iota
Medium
High
Critical
)
func (p Priority) String() string {
switch p {
case Low: return "low"
case Medium: return "medium"
case High: return "high"
case Critical: return "critical"
}
return fmt.Sprintf("Priority(%d)", int(p))
}
Solution 2¶
type NotFoundError struct{ Resource string }
func (e *NotFoundError) Error() string {
return e.Resource + " not found"
}
Solution 3¶
type StringReader struct {
s string
pos int
}
func (r *StringReader) Read(p []byte) (int, error) {
if r.pos >= len(r.s) {
return 0, io.EOF
}
n := copy(p, r.s[r.pos:])
r.pos += n
return n, nil
}
Solution 4¶
type CountingWriter struct{ N int64 }
func (c *CountingWriter) Write(p []byte) (int, error) {
c.N += int64(len(p))
return len(p), nil
}
Solution 5¶
type DescInts []int
func (a DescInts) Len() int { return len(a) }
func (a DescInts) Less(i, j int) bool { return a[i] > a[j] }
func (a DescInts) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// usage: sort.Sort(DescInts(xs))
Solution 6¶
type Profile struct {
Name string
Bio string
Email *string
}
func (p Profile) MarshalJSON() ([]byte, error) {
out := map[string]any{"name": p.Name}
if p.Bio != "" {
out["bio"] = p.Bio
}
if p.Email != nil {
out["email"] = *p.Email
}
return json.Marshal(out)
}
Solution 7¶
type UUID [16]byte
func (u UUID) MarshalText() ([]byte, error) {
buf := make([]byte, 36)
hex.Encode(buf[0:8], u[0:4])
buf[8] = '-'
hex.Encode(buf[9:13], u[4:6])
buf[13] = '-'
hex.Encode(buf[14:18], u[6:8])
buf[18] = '-'
hex.Encode(buf[19:23], u[8:10])
buf[23] = '-'
hex.Encode(buf[24:36], u[10:16])
return buf, nil
}
func (u *UUID) UnmarshalText(text []byte) error {
if len(text) != 36 {
return fmt.Errorf("UUID: invalid length %d", len(text))
}
s := string(text)
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return errors.New("UUID: invalid format")
}
raw := s[0:8] + s[9:13] + s[14:18] + s[19:23] + s[24:36]
decoded, err := hex.DecodeString(raw)
if err != nil {
return fmt.Errorf("UUID: %w", err)
}
copy(u[:], decoded)
return nil
}
Solution 8¶
type Buf struct{ data []byte }
func (b *Buf) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(b.data)
return int64(n), err
}
// io.Copy(dst, &Buf{data: payload}) calls WriteTo directly, skipping
// the 32 KiB buffer loop.
Solution 9¶
type JSONHandler[T any] struct {
Fn func(*http.Request) (T, error)
}
func (h JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
v, err := h.Fn(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(v); err != nil {
log.Println("encode:", err)
}
}
Solution 10¶
func Download(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { return nil, err }
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Solution 11¶
type StringReadCloser struct {
s string
pos int
closed bool
}
func (r *StringReadCloser) Read(p []byte) (int, error) {
if r.closed { return 0, os.ErrClosed }
if r.pos >= len(r.s) { return 0, io.EOF }
n := copy(p, r.s[r.pos:])
r.pos += n
return n, nil
}
func (r *StringReadCloser) Close() error {
if r.closed { return os.ErrClosed }
r.closed = true
return nil
}
Solution 12¶
type People []Person
func (p People) Len() int { return len(p) }
func (p People) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p People) Less(i, j int) bool {
if p[i].LastName != p[j].LastName { return p[i].LastName < p[j].LastName }
if p[i].FirstName != p[j].FirstName { return p[i].FirstName < p[j].FirstName }
return p[i].Age < p[j].Age
}
Solution 13¶
type Money struct {
Cents int64
Currency string
}
func (m Money) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "Money{cents=%d currency=%s}", m.Cents, m.Currency)
return
}
fallthrough
case 's':
sign, c := "", m.Cents
if c < 0 { sign, c = "-", -c }
fmt.Fprintf(s, "%s%d.%02d %s", sign, c/100, c%100, m.Currency)
case 'd':
fmt.Fprintf(s, "%d", m.Cents)
default:
fmt.Fprintf(s, "%%!%c(Money)", verb)
}
}
Solution 14¶
type gzipWriterTo struct{ src io.Reader }
func (g *gzipWriterTo) WriteTo(w io.Writer) (int64, error) {
cw := &countingWriter{w: w}
zw := gzip.NewWriter(cw)
if _, err := io.Copy(zw, g.src); err != nil {
zw.Close()
return cw.n, err
}
if err := zw.Close(); err != nil { return cw.n, err }
return cw.n, nil
}
type countingWriter struct {
w io.Writer
n int64
}
func (c *countingWriter) Write(p []byte) (int, error) {
n, err := c.w.Write(p)
c.n += int64(n)
return n, err
}
Solution 15¶
type Roles []string
func (r Roles) Value() (driver.Value, error) {
return strings.Join(r, ","), nil
}
func (r *Roles) Scan(src any) error {
var s string
switch v := src.(type) {
case string: s = v
case []byte: s = string(v)
case nil: *r = nil; return nil
default:
return fmt.Errorf("Roles.Scan: unsupported type %T", src)
}
if s == "" { *r = nil; return nil }
*r = strings.Split(s, ",")
return nil
}
Solution 16¶
type Event struct {
ID int `json:"id"`
Time time.Time `json:"time"`
}
func eventsHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
enc := json.NewEncoder(w)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for id := 0; ; id++ {
select {
case <-r.Context().Done():
return
case t := <-ticker.C:
fmt.Fprint(w, "data: ")
if err := enc.Encode(Event{ID: id, Time: t}); err != nil { return }
fmt.Fprint(w, "\n")
flusher.Flush()
}
}
}
Solution 17¶
func Filter[V any](in iter.Seq[V], pred func(V) bool) iter.Seq[V] {
return func(yield func(V) bool) {
for v := range in {
if !pred(v) { continue }
if !yield(v) { return }
}
}
}
// Test:
// evens := Filter(slices.Values([]int{1,2,3,4}), func(v int) bool { return v%2 == 0 })
// got := slices.Collect(evens) // [2 4]
Solution 18¶
type Blob []byte
type BlobCursor struct {
b Blob
pos int64
closed bool
}
func (c *BlobCursor) Read(p []byte) (int, error) {
if c.closed { return 0, os.ErrClosed }
if c.pos >= int64(len(c.b)) { return 0, io.EOF }
n := copy(p, c.b[c.pos:])
c.pos += int64(n)
return n, nil
}
func (c *BlobCursor) Seek(offset int64, whence int) (int64, error) {
if c.closed { return 0, os.ErrClosed }
var abs int64
switch whence {
case io.SeekStart: abs = offset
case io.SeekCurrent: abs = c.pos + offset
case io.SeekEnd: abs = int64(len(c.b)) + offset
default: return 0, errors.New("BlobCursor: invalid whence")
}
if abs < 0 { return 0, errors.New("BlobCursor: negative position") }
c.pos = abs
return abs, nil
}
func (c *BlobCursor) Close() error {
c.closed = true
return nil
}
Solution 19¶
func upgradeWS(w http.ResponseWriter, r *http.Request) (net.Conn, error) {
hj, ok := w.(http.Hijacker)
if !ok { return nil, errors.New("hijack not supported") }
key := r.Header.Get("Sec-WebSocket-Key")
if key == "" { return nil, errors.New("missing Sec-WebSocket-Key") }
h := sha1.New()
h.Write([]byte(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
accept := base64.StdEncoding.EncodeToString(h.Sum(nil))
conn, bufrw, err := hj.Hijack()
if err != nil { return nil, err }
fmt.Fprintf(bufrw,
"HTTP/1.1 101 Switching Protocols\r\n"+
"Upgrade: websocket\r\n"+
"Connection: Upgrade\r\n"+
"Sec-WebSocket-Accept: %s\r\n\r\n", accept)
if err := bufrw.Flush(); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
Solution 20¶
type TarFS struct {
entries map[string][]byte
}
type tarFile struct {
name string
data *bytes.Reader
mode fs.FileMode
}
func (f *tarFile) Stat() (fs.FileInfo, error) {
return &tarInfo{name: f.name, size: int64(f.data.Len()), mode: f.mode}, nil
}
func (f *tarFile) Read(p []byte) (int, error) { return f.data.Read(p) }
func (f *tarFile) Close() error { return nil }
type tarInfo struct {
name string
size int64
mode fs.FileMode
}
func (i *tarInfo) Name() string { return path.Base(i.name) }
func (i *tarInfo) Size() int64 { return i.size }
func (i *tarInfo) Mode() fs.FileMode { return i.mode }
func (i *tarInfo) ModTime() time.Time { return time.Time{} }
func (i *tarInfo) IsDir() bool { return i.mode.IsDir() }
func (i *tarInfo) Sys() any { return nil }
func (t *TarFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
if name == "." {
return &tarFile{name: ".", data: bytes.NewReader(nil), mode: fs.ModeDir}, nil
}
data, ok := t.entries[name]
if !ok {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
return &tarFile{name: name, data: bytes.NewReader(data)}, nil
}
Solution 21¶
type ridKey struct{}
type userKey struct{}
type traceKey struct{}
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, ridKey{}, id)
}
func WithUser(ctx context.Context, user string) context.Context {
return context.WithValue(ctx, userKey{}, user)
}
func WithTrace(ctx context.Context, trace string) context.Context {
return context.WithValue(ctx, traceKey{}, trace)
}
func RequestIDFrom(ctx context.Context) string {
v, _ := ctx.Value(ridKey{}).(string)
return v
}
// Cancellation propagates because each WithValue wraps the parent without
// breaking the Done channel chain.
//
// Verification:
// root, cancel := context.WithCancel(context.Background())
// c1 := WithRequestID(root, "r-1")
// c2 := WithUser(c1, "alice")
// cancel()
// <-c2.Done() // unblocks immediately
Solution 22¶
type marshalerFn func(any) ([]byte, error)
var (
registry = map[reflect.Type]marshalerFn{}
registryMu sync.RWMutex
)
func RegisterMarshaler[T any](fn func(T) ([]byte, error)) {
var zero T
typ := reflect.TypeOf(zero)
registryMu.Lock()
registry[typ] = func(v any) ([]byte, error) { return fn(v.(T)) }
registryMu.Unlock()
}
func Marshal(v any) ([]byte, error) {
typ := reflect.TypeOf(v)
registryMu.RLock()
fn, ok := registry[typ]
registryMu.RUnlock()
if ok { return fn(v) }
return json.Marshal(v)
}
// Usage:
// RegisterMarshaler(func(t time.Time) ([]byte, error) {
// return []byte(`"` + t.Format(time.RFC3339) + `"`), nil
// })
// buf, _ := Marshal(time.Now())