---
title: 'The Standard Library and Network Applications'
source: 'https://academia.sh/en/courses/go-applications'
language: en
updated: '2026-09-14T13:07:01+00:00'
license: 'CC BY-SA 4.0'
lessons: 12
outcomes:
    - 'Writes an HTTP service and a command-line tool with the standard library'
    - 'Uses serialization, logging and time operations correctly'
    - 'Builds database access with connection management'
    - "Distinguishes by measurement what happens to a value's type information as it crosses a boundary"
---

# The Standard Library and Network Applications

The twelve lessons of this course describe a single thing: the boundaries of a program. The network, a file, template output, a database row, a log record and a command-line argument are separate faces of the same event — a value leaves the type system and reappears on the other side. The course measures every crossing with one question: is type information preserved, rebuilt, or lost?

## Networking and Data Formats

Building request handlers and the middleware pattern in the standard library, the client side's timeout and body-closing responsibility, carrying field names across with struct tags, templates producing only text, contract-based communication with a schema shared by both sides, and the single piece of information framing carries in a long-lived connection.

### HTTP Server

Source: https://academia.sh/en/courses/go-applications/http-server

The course's opening lesson. That none of a request's path, header, and body carry type information is measured with `net/http/httptest`, without opening a real port; the middleware pattern runs as a three-ring chain in which one handler wraps another. The limiting measure: the content type header is a hint, not a type — a wrong hint produces no error on the server side, the error only surfaces on the side that tries to resolve the body against that hint.

The previous course's closing lesson measured how a goroutine stops and showed that
stopping mostly depends on **work done by someone else**: without a channel closing,
without a context being canceled, a waiting goroutine could not make its own decision. That
measurement always stayed **inside** the program — the two goroutines were part of the same
compiled binary, bound to the same type system. This lesson carries the question **outside**
the program: what happens when a value goes to the network, a file, a template, or a
database? The other side did not pass through the same compiler, did not see the same type
definitions — sometimes it was not even written in the same language. The whole course asks
this one question at twelve different boundaries: **what happens to a value's type
information as it crosses a boundary?** This lesson starts with the boundary crossed most
often: an HTTP request.

What a server does, who handles which request when, and how middleware is ordered in a
chain were already built as concepts in earlier courses. None of that is repeated here; this
lesson asks a different question — not the server's **task**, but the relationship between
the data **entering and leaving the server** and the type system. This distinction holds for
the rest of the course: every lesson measures not a layer's task but the fate of the data at
that layer's boundary.

#### What the Handler Receives: Path, Header, Body

When a client sends an HTTP request, the only source for what is inside that request is the
byte sequence that crosses the wire. Go's standard library parses this byte sequence and
turns it into an `*http.Request` value, but what gets turned into it is still three raw
pieces: the **path** (`r.URL.Path`, a string), the **headers** (`r.Header`, a map from
string to string slice), and the **body** (`r.Body`, a reader that streams bytes). In the
sending side's mind the path might be an integer ID, the header a structured value, the
body a record — but after crossing the boundary, what is left is only strings and bytes.
Go's type system says "this was really an integer" or "this was really a struct" about none
of these three; it has no authority to say so, because it does not know which program is
running on the other end of the wire or which types that program defines.

The source below builds this measurement's shared parts: a helper that classifies and
prints results, a core handler that reads the request's path and one header and writes the
body's length as the response, and two middleware layers that wrap this core.

```go
// requestcore.go — the shared parts of the request handler and middleware
package main

import (
	"fmt"
	"io"
	"net/http"
)

var partCount, lostCount int
var lastReadBody []byte

// measureLine prints one of this lesson's classes (preserved / rebuilt / lost)
// alongside a piece of evidence and keeps a running total.
func measureLine(part, class, evidence string) {
	partCount++
	if class == "lost" {
		lostCount++
	}
	fmt.Printf("%-30s %-10s %s\n", part, class, evidence)
}

// coreHandler reads the incoming request's path, one header, and body, and
// writes a plain-text response. It adds a fixed content type hint to the
// response; checking whether that hint is correct is not this function's job.
func coreHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	lastReadBody = body
	id := r.Header.Get("X-Request-ID")
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "path=%s id=%s body-length=%d", r.URL.Path, id, len(body))
}

// loggingMiddleware increments a counter before calling the handler it wraps;
// it does not touch the body or the connection.
func loggingMiddleware(next http.Handler, counter *int) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		*counter++
		next.ServeHTTP(w, r)
	})
}

// headerMiddleware adds a fixed header to the response and continues the chain.
func headerMiddleware(next http.Handler, counter *int) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		*counter++
		w.Header().Set("X-Chain-Step", "passed")
		next.ServeHTTP(w, r)
	})
}
```

The measurement runs this core with one request. The request is built with
`net/http/httptest` — `httptest.NewRequest` produces an `*http.Request` in memory without
opening a real port, and `httptest.NewRecorder` gives a fake `http.ResponseWriter` that
writes the response to a buffer instead of a real connection. There is no real socket,
address, or duration between client and server.

```go
// requestmeasurement.go — the type information of a request's parts and the middleware chain
package main

import (
	"bytes"
	"fmt"
	"net/http"
	"net/http/httptest"
)

func main() {
	fmt.Println("-- what the handler receives: bytes and text --")

	body := bytes.NewBufferString("qty=3")
	request := httptest.NewRequest(http.MethodPost, "/products/42", body)
	request.Header.Set("X-Request-ID", "1001")

	var logCount, headerCount int
	chain := loggingMiddleware(
		headerMiddleware(http.HandlerFunc(coreHandler), &headerCount),
		&logCount,
	)

	recorder := httptest.NewRecorder()
	chain.ServeHTTP(recorder, request)

	measureLine("request path (r.URL.Path)", "lost",
		fmt.Sprintf("value=%q type=%T", request.URL.Path, request.URL.Path))
	measureLine("request header (Header.Get)", "lost",
		fmt.Sprintf("value=%q type=%T", request.Header.Get("X-Request-ID"), request.Header.Get("X-Request-ID")))
	measureLine("request body (bytes read)", "lost",
		fmt.Sprintf("type=%T length=%d", lastReadBody, len(lastReadBody)))

	fmt.Println("-- middleware chain --")
	fmt.Println("chain: loggingMiddleware( headerMiddleware( coreHandler ) ) - 3 rings")
	fmt.Printf("logging calls=%d header calls=%d (both written by this lesson)\n",
		logCount, headerCount)
	fmt.Printf("status code=%d chain header=%s\n", recorder.Code, recorder.Header().Get("X-Chain-Step"))
	fmt.Println("response body:", recorder.Body.String())

	fmt.Printf("of this request's %d parts, carrying type information=%d (lost=%d)\n",
		partCount, partCount-lostCount, lostCount)
}
```

```
-- what the handler receives: bytes and text --
request path (r.URL.Path)      lost       value="/products/42" type=string
request header (Header.Get)    lost       value="1001" type=string
request body (bytes read)      lost       type=[]uint8 length=5
-- middleware chain --
chain: loggingMiddleware( headerMiddleware( coreHandler ) ) - 3 rings
logging calls=1 header calls=1 (both written by this lesson)
status code=200 chain header=passed
response body: path=/products/42 id=1001 body-length=5
of this request's 3 parts, carrying type information=0 (lost=3)
```

**ND1.** The path being `"/products/42"` suggests that the digit `42` should **mean** an
integer — in the source code this value was a product ID. But when the measurement asks with
`%T`, the answer is uniform: `string`. Go's router does not automatically convert any digit
sequence to a number while parsing the path; `/products/42` and `/products/forty-two` are two
strings of the same type, and the only difference between them is their content. The same
observation holds for the header: the value `"1001"` in the request ID header is also a
string, even if it is a numeric ID in the sending side's mind. Nothing that crosses the wire
carries its own type; the only thing it carries is characters.

**ND2.** The body is the clearest of these three: `io.ReadAll(r.Body)` always returns
`[]byte`, regardless of whether the body's content is a number, a struct, or random text. The
measurement shows `length=5` because the string `"qty=3"` is five bytes — but this length
does not say **what** the body represents, only **how many bytes** it is. All three of these
lines fall into the `lost` class; of the request's three parts measured by this lesson, zero
carry type information. The full distribution of the course's thirteen boundary crossings (1
preserved, 5 rebuilt, 7 lost) is built in the JSON Serialization lesson; this lesson only
prints its own three-line slice.

#### Middleware: One Handler Wrapping Another

`http.Handler` is an interface defined by a single method in Go's standard library:
`ServeHTTP(http.ResponseWriter, *http.Request)`. This smallness is a deliberate design
decision — a handler can be **anything** that implements this single method, so embedding
one handler inside another needs no special framework, an ordinary function call is enough.
Both `loggingMiddleware` and `headerMiddleware` follow the same pattern: each takes an
`http.Handler`, returns a new `http.HandlerFunc` that runs its own logic, and inside the
body of the handler it returns, calls the `ServeHTTP` method of the handler it received.
The result is a **chain**: the outer ring runs first, does its work, and calls the inner
ring, which does its own work and calls the next one — until the core handler, at the
innermost point, stops the chain.

In the source code this chain is built by hand with a single statement in the `main`
function, by placing the `headerMiddleware` call inside the `loggingMiddleware` call, and
the core handler inside that: three rings, all three written by this lesson, none discovered
automatically from a registry or a tag. The output lines `logging calls=1` and
`header calls=1` count this directly: both middleware layers in the chain were called
exactly once, because the request also arrived exactly once. What a server's task is, the
general structure of a request-processing chain, and why middleware exists were already
built as concepts in earlier courses; what is measured here is that concept's concrete
counterpart in Go's standard library — there is no hidden mechanism beyond one handler
wrapping another.

**ND3.** The chain's order affects the outcome. `headerMiddleware` sets the `X-Chain-Step`
header inside its own `ServeHTTP` body, **before** calling the inner handler; but this
header is only locked into the response the moment `w.WriteHeader` is called, and it is
`coreHandler` that calls `WriteHeader`. So the header `headerMiddleware` adds succeeds in
reaching the response, because it is set before the core writes the response — the output
line `chain header=passed` confirms this. If the order were reversed, that is, if
`headerMiddleware` ran **after** the core, the header would be silently dropped because
`WriteHeader` would already have been called — Go does not error in this case, it just
ignores the header — unless the status code is in the 1xx class and the header is marked as
a trailer.

**ND4.** Both middleware layers share the same signature: each takes an `http.Handler` and a
counter pointer and returns a new `http.Handler`. This uniformity reduces adding a third
middleware to the same chain to a one-line wrap — adding a new ring to the chain is not a
different operation from adding a new element to a list, it is just placing one function
call inside the previous one. But this convenience has a cost: the longer the chain, the
more nested function calls must be read to see how many rings a request passes through
before reaching `coreHandler`. The chain itself does not stand anywhere as a list — it is
only a single statement in the `main` function.

This also determines the chain's relationship to **type information**: neither
`loggingMiddleware` nor `headerMiddleware` looks at the content of the request or the
response to do its job; both only pass the `*http.Request` and `http.ResponseWriter`
values on to the next ring as they are. If a middleware layer wanted to read a field in the
request's body, it would run into the same boundary seen throughout this lesson — the body
would still be only bytes, and the middleware could only assign meaning to those bytes with
parsing code it wrote itself.

#### The Limiting Measure: Content Type Is a Hint, Not a Type

The `Content-Type: application/json` header the core handler adds to its response looks, at
first glance, like a guarantee of the response body's type. It is not. This header is
nothing more than a string, and no mechanism steps in to check whether the body is really in
that format, no matter who reads it. The measurement below deliberately breaks this: the
handler gives an `application/json` hint but writes plain text to the body. This is a
different observation from the three `lost` lines in the previous measurement: those lines
measured that the information the request carried **never existed at all**, while this
measurement shows that a piece of information that does exist (the header itself) **can be
wrong** and that nothing catches this anywhere.

```go
// wronghint.go — the content type is a hint; its error surfaces not while running but while resolving
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
)

// fakeHandler writes plain text to the body and gives an "application/json"
// hint in the header. No code checks the agreement between the hint and the body.
func fakeHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, "this body is not json, it is plain text")
}

func main() {
	fmt.Println("-- content type is a hint, not a type --")

	request := httptest.NewRequest(http.MethodGet, "/report", nil)
	recorder := httptest.NewRecorder()
	http.HandlerFunc(fakeHandler).ServeHTTP(recorder, request)

	fmt.Printf("status code=%d (succeeds even when the hint is wrong)\n", recorder.Code)
	fmt.Printf("hint in the header=%s\n", recorder.Header().Get("Content-Type"))

	var target map[string]any
	decodeErr := json.Unmarshal(recorder.Body.Bytes(), &target)
	fmt.Printf("is there an error decoding the body as json=%t\n", decodeErr != nil)
	fmt.Println("the error is not on the server, it surfaces on the side trying to resolve the body against the hint")
}
```

```
-- content type is a hint, not a type --
status code=200 (succeeds even when the hint is wrong)
hint in the header=application/json
is there an error decoding the body as json=true
the error is not on the server, it surfaces on the side trying to resolve the body against the hint
```

**ND5.** The status code is `200` — that is, the handler completed successfully without
ever noticing that the body disagreed with the hint. Nothing in the `http.ResponseWriter`
interface compares the `Content-Type` header against the body, or checks whether the body is
really valid JSON — the header is a suggestion telling the receiver "try interpreting this
body this way," not a rule the receiver must obey. The error only surfaces once one side
trusts this suggestion and calls `json.Unmarshal`; in the measurement this call happens in
the `main` function itself, and the result `decodeErr != nil` comes out `true`. There is no
warning, no log line, no error return on the server side — the boundary crossing itself is
entirely silent.

**ND6.** This observation sharpens the lesson's opening claim: the **only** hint a response
carries alongside it as it crosses the boundary is a header like this, and this hint is not
a type, it is a string — just like `r.URL.Path` or `r.Header.Get`, which also came out as
`string` when asked with `%T`. Whether the content type header is correct is left to the
discipline of the side writing the server; neither Go's compiler nor its runtime enforces
this discipline. This limiting measure shows that the information the body itself carries
and the information written in the header are two **independent** things, and that neither
confirms the other.

The standard library itself adds no check that ties the two together; the side writing the
handler is the one that would add it. If it wanted to, `coreHandler` could validate in its
own code, before the `w.Write` call, that the body is really valid JSON — but this
validation is not part of the language, it is a decision made by the writer. The only
guarantee that holds on this side of the boundary is that **nobody automatically ensures**
the consistency between header and body; every layer that establishes this consistency has
to do so explicitly in its own code.

#### Summary

- An HTTP request's path, headers, and body are represented in Go's standard library as a
  string, a string, and a byte slice respectively; all three turn out to carry no special
  type when queried with `%T`.
- The measurement is built with `net/http/httptest` without opening a real port:
  `httptest.NewRequest` produces a request in memory, `httptest.NewRecorder` supplies a fake
  response writer that writes the response to a buffer.
- The middleware pattern, thanks to the `http.Handler` interface having a single method,
  amounts to nothing more than wrapping one handler inside another; every ring in the chain
  is an ordinary function call written by the caller, with no automatic discovery or
  registry.
- The order in which middleware runs affects the outcome: a header only reaches the response
  if it is added before `WriteHeader` is called; if the order is reversed, the header is
  dropped without an error.
- The content type header is a hint, not a guarantee: a wrong hint produces no error on the
  server side, the error only surfaces on the side that tries to resolve the body by
  trusting that hint.
- The full distribution of the course's thirteen boundary crossings is not built in this
  lesson; this lesson only measures the request's three parts on the server side, the JSON
  Serialization lesson builds the full table.

#### Next Step

This lesson measured the boundary from the **receiving** side: what a request brings to the
server, and how blindly the server trusts the body. The next lesson asks the same boundary
from the **sending** side: what a client has to guarantee when sending a request, and what
responsibilities it takes on after receiving a response. A response body is a byte stream,
just like this lesson's request body, but this time what is measured is who closes the
stream and when, and what a timeout is built from instead of a duration.

### HTTP Client

Source: https://academia.sh/en/courses/go-applications/http-client

The previous lesson measured the boundary from the receiving side; this lesson measures the same boundary from the sending side. A timeout is built not with a duration but with an already-canceled context. Whether a connection gets reused depends on whether the response body is closed: once the body is read and closed, the next request uses the same connection; if it is not closed, the next request does not. The limiting measure: the response body is a reader, not a value — once read, a second read returns empty.

The previous lesson measured the boundary from the **receiving** side: what a request
brings to the server, and how blindly the server trusts the body. This lesson asks the same
boundary from the **sending** side. A client takes on three responsibilities when it sends a
request: ensuring the request resolves in a reasonable time, reusing connections that talk
to the same server, and closing the body of the response it receives. These three might look
independent of each other, but the measurement below shows that two of them (connection
reuse and closing the body) are actually two faces of **the same decision**.

This lesson also uses `net/http/httptest` like the previous one; but this time it is not
`httptest.NewRecorder`, it is `httptest.NewServer` — a server running in memory that the
client can really open and close a connection against. The server's address (`server.URL`)
is only used to build requests, never printed in any measurement line; what is measured is
not the address itself but how the client behaves against that address. This difference is
itself part of the measurement: the previous lesson could run its server without ever
touching the network, because what it measured was only the content of the request and the
response; this lesson measures the client's connection management, so it needs a real (but
nowhere printed) local connection.

The three responsibilities are handled in order. The first is the guarantee that a request
will not wait forever. The second is avoiding the cost of establishing a new connection for
every one of a series of successive requests. The third is closing the body of a received
response instead of abandoning it — and the measurement below shows that this third
responsibility is actually the condition that makes the second one possible.

#### Timeout: Not a Duration, a Canceled Context

The pattern established in the previous course's Context Package lesson continues here
exactly: a timeout is built not with `time.Sleep` or a real duration measurement, but with a
context that has been **already canceled**. This keeps the lesson's output from changing
between runs — if a real timeout were tried, how long the request would take to fail would
depend on the machine and the load.

```go
// servercore.go — the shared server body for client measurements
package main

import (
	"fmt"
	"net/http"
)

// responderHandler is a handler that returns a fixed text body for every
// request. The body is kept small so reading and closing it does not affect the measurement.
func responderHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")
	fmt.Fprint(w, "qty=7 unit=kg")
}

// measureLine prints one of the three outcomes at the boundary (preserved / rebuilt /
// lost) alongside a piece of evidence.
func measureLine(part, class, evidence string) {
	fmt.Printf("%-30s %-10s %s\n", part, class, evidence)
}
```

```go
// timeout.go — a timeout built not with a duration, but with an already-canceled context
package main

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
)

func main() {
	fmt.Println("-- timeout: not with a duration, but with an already-canceled context --")

	server := httptest.NewServer(http.HandlerFunc(responderHandler))
	defer server.Close()

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // the context was already canceled before the request even started

	request, _ := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)
	_, err := http.DefaultClient.Do(request)

	fmt.Printf("is there a request error=%t\n", err != nil)
	fmt.Printf("does the error match the context cancellation=%t\n", errors.Is(err, context.Canceled))
}
```

```
-- timeout: not with a duration, but with an already-canceled context --
is there a request error=true
does the error match the context cancellation=true
```

**ND7.** The `cancel()` call runs **before** the request is sent; this ordering is
deliberate, because the measurement's goal is to leave no room for any argument about
timing. `http.DefaultClient.Do` is called while the context is already canceled, and the
client returns an error immediately, without ever putting the request on the wire. The check
`errors.Is(err, context.Canceled)` comes out `true` — the returned error is not one the
network layer produced itself, it is the wrapped form of the fixed error the context
carries. If a real timeout (`context.WithTimeout`) had been set up, the same mechanism would
work, only the error would be `context.DeadlineExceeded` instead of `context.Canceled`; both
come from the same `ctx.Err()` path.

**ND8.** This measurement asks, at this boundary, the same question the previous course's
closing lesson left open: what stopped a goroutine — a checkpoint in its own code path.
`http.Client` follows the same principle: it checks the context's cancellation in its own
internal loop (while establishing the connection, while waiting for the response), and stops
and returns the error once it reaches this checkpoint. The difference is that this time the
checkpoint is not visible in the source code — the client itself is part of the standard
library, but the behavior is the same: a context is not a forcing tool, it is a source of
news, and `http.Client` is one of the parties listening to that news. This consistency comes
from the standard library holding its own internal functions to the same principle; once the
context package is learned, it behaves the same way regardless of which side of a boundary
it is on.

No real duration was used at all in this measurement; calling `cancel()` immediately after
`context.WithCancel` makes it **certain** that the request will fail, completely removing
the uncertainty of "it might make it in time, it might not." If a real `context.WithTimeout`
had been used, the measurement's result would depend on the speed of the machine the
verifier runs on — the same source would time out on some runs and not on others, because
the server would respond fast enough. An already-canceled context moves this uncertainty
from run time to design time.

#### Connection Reuse: The Cost of Closing the Body

Establishing a TCP connection is not a cheap operation; this is why Go's `http.Transport`
keeps used connections in a **connection pool** and tries to reuse them for later requests.
The connection pool concept itself was built in the Data Access Layer course; what is
measured here is under what condition Go's standard library `http.Transport` operates this
pool. This reuse is not unconditional: unless a response's body is fully read and closed,
the connection underneath it does not go back into the pool. This is a responsibility
independent from the timeout in the previous section — even if the request completes
successfully, even if the status code returns `200`, ignoring the body can **cost the
connection**.

The measurement below observes this with `net/http/httptrace`: the `GotConn` field of
`httptrace.ClientTrace` reports, for every request, whether the connection obtained was
**reused** (the `Reused` field, a `bool`). This package is also part of the standard library
and exposes no real address or port, it only gives a logical flag.

The previous section used `http.DefaultClient` while measuring the timeout, because what was
measured there was context cancellation, and which client was used did not change the
outcome. This section explicitly builds its own clients, because what is measured here is
precisely the connection pool a client carries — using `http.DefaultClient` would make this
measurement liable to mix with connections left behind by code running in another lesson of
the course. This is the first sign that an HTTP client carries invisible state (open
connections): two different `*http.Client` values, even against the same server, can
accumulate a completely different history from each other.

```go
// connectionreuse.go — closing the body determines whether the connection returns to the pool
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/http/httptrace"
)

// requestWithTrace sends a request with the given client; learns whether the
// connection was reused with httptrace and closes the body according to closeBody.
func requestWithTrace(client *http.Client, url string, closeBody bool) (reused bool) {
	ctx := httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{
		GotConn: func(info httptrace.GotConnInfo) { reused = info.Reused },
	})
	request, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	response, err := client.Do(request)
	if err != nil {
		return false
	}
	if closeBody {
		io.ReadAll(response.Body)
		response.Body.Close()
	}
	return reused
}

func main() {
	fmt.Println("-- connection reuse: with the body closed and left unclosed --")

	server := httptest.NewServer(http.HandlerFunc(responderHandler))
	defer server.Close()

	// Each client carries its own Transport, so the connection pools stay
	// independent of each other and an unclosed body on one client does not affect the other.
	clientA := &http.Client{Transport: &http.Transport{}}
	firstA := requestWithTrace(clientA, server.URL, true)
	secondA := requestWithTrace(clientA, server.URL, true)
	fmt.Printf("body closed each time: first=%t second=%t\n", firstA, secondA)

	clientB := &http.Client{Transport: &http.Transport{}}
	requestWithTrace(clientB, server.URL, false)
	secondB := requestWithTrace(clientB, server.URL, true)
	fmt.Printf("first request's body never closed: second=%t\n", secondB)
}
```

```
-- connection reuse: with the body closed and left unclosed --
body closed each time: first=false second=true
first request's body never closed: second=false
```

**ND9.** In the two requests made with `clientA`, the body is read and closed every time.
`Reused=false` on the first request is an expected result, since there is not yet an open
connection; what is actually measured is the **second** request. Because the body was fully
read and closed on the first request, the connection has already gone back into the pool,
and the second request inherits the same connection with `Reused=true`. This shows why the
client side's third responsibility (closing the body) is not just "cleanup" but a direct
performance decision.

Each of `clientA` and `clientB` carries its own `*http.Transport` value; this is a
deliberate choice. If the `Transport` field were left empty, both would use the standard
library's **shared** default transport, and the two clients' connection pools would actually
be the same pool — `clientB`'s unclosed body could affect `clientA`'s next request too, even
though they have different names. Giving each its own `Transport` **isolates** the two
scenarios from each other: the measured effect really comes from `clientB`'s own unclosed
body, not from a side effect of another request.

**ND10.** In the first request made with `clientB`, the body is never read, never closed.
The second request is sent with the same client, with its body properly closed, but the
result is still `Reused=false`. The reason is that the first request's connection is still
considered **busy**: `Transport` never puts a connection whose response body was left unread
and unclosed back into the pool, because it does not know when that connection will become
free. The second request is therefore forced to open a new connection — the first request's
unclosed body also affects the request that comes after it.

It is clear that this effect grows with scale: a program that sends hundreds of successive
requests but forgets to close some of its bodies keeps one connection busy forever for every
unclosed body. The program itself does not crash, does not error either — only the client's
open connection count builds up over time, and this buildup stays as silent as the body
itself.

**ND11.** The rule that emerges when these two measurements are read together is clear:
connection reuse gives a single answer to the question of **whose job** it is — the
caller's. The standard library does not automatically read and close the body; if
`Body.Close()` is never called, or even if it is called but the body is not first fully
read, the connection leaks, or at least cannot be reused. This stands in the same place as
the principle seen in the previous lesson's middleware chain: the standard library offers a
mechanism on this side of the boundary, but using that mechanism correctly stays the
responsibility of the side that writes the code.

This rule also adds an example to the type system's role at the boundary: the type of
`response.Body` (`io.ReadCloser`) itself says nowhere "do not close me before fully reading
me." The compiler does not reject a response whose body is left unread — the source compiles
fine, runs fine, only the connection pool quietly shrinks. A contract of this kind (that a
resource must be consumed before it is closed) lives not in the type itself but in its
documentation and in the habits of the side that writes the code.

#### The Limiting Measure: The Body Is a Reader, Not a Value

The previous two measurements showed **why** closing the body matters. This measurement
clarifies **what** the body is: `response.Body` is a stream implementing the
`io.ReadCloser` interface, consumed once just like the request body, and nothing is left
once it is consumed. This is a fundamentally different behavior from the copyable values
seen in earlier courses of the language (a slice, a struct): assigning a slice to two
variables and reading both of them separately was possible, because a slice was a value. A
reader carries not a value but a **position** — that position advances as it is read, and
does not go back.

```go
// bodyreading.go — the response body is a reader, not a value
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
)

func main() {
	fmt.Println("-- the response body is a reader, it cannot be read twice --")

	server := httptest.NewServer(http.HandlerFunc(responderHandler))
	defer server.Close()

	response, _ := http.Get(server.URL)
	defer response.Body.Close()

	firstRead, _ := io.ReadAll(response.Body)
	secondRead, secondErr := io.ReadAll(response.Body)

	measureLine("response body (read on the client)", "lost",
		fmt.Sprintf("type=%T first-length=%d", firstRead, len(firstRead)))
	fmt.Printf("second read length=%d error=%v\n", len(secondRead), secondErr)
}
```

```
-- the response body is a reader, it cannot be read twice --
response body (read on the client) lost       type=[]uint8 first-length=13
second read length=0 error=<nil>
```

**ND12.** The first `io.ReadAll` call reads the body from start to end and returns a
thirteen-byte slice — what reaches this side of the boundary is again `[]byte`, the same
class seen in the previous lesson: **lost**. The second `io.ReadAll` call returns a result of
length `0` and a `nil` error; because nothing is left to read, the read operation finishes
instantly, but this does not count as an error. If the body were a slice or a string, it
would be possible to read the same value as many times as wanted; because it is a reader,
the moment it is consumed, only emptiness remains behind it.

**ND13.** The practical consequence of this behavior: a response body can only be parsed
**once**. If a function wants to both log the body and decode it to a target, it has to
first copy the body into a buffer (like `bytes.Buffer`) and do the two different reads
through that buffer — returning to the source itself a second time is not possible. This
limitation is also connected to the connection reuse from the previous section: copying the
body into a buffer means fully consuming it at the same time, so this copying operation both
makes a second read possible and lets the connection underneath go back into the pool — the
same `io.ReadAll` call solves two separate problems at once.

The full distribution of the course's thirteen boundary crossings (1 preserved, 5 rebuilt, 7
lost) is built in the next lesson, JSON Serialization; this lesson only measured the client
side's three responsibilities and the body's nature as a reader.

#### Summary

- A timeout is built with an already-canceled context instead of a duration; `http.Client`
  checks the context's cancellation at its own internal checkpoints and stops the request
  there.
- The request error checked with `errors.Is` is the wrapped form of the fixed error the
  context carries (`context.Canceled` or `context.DeadlineExceeded`).
- Whether a connection will be reused is measured with the `httptrace.GotConnInfo.Reused`
  field; unless the body is fully read and closed, the connection does not return to the
  pool.
- The effect of a request whose body was not closed also reaches the request after it: a
  new connection opens because the previous connection is still considered busy.
- The response body is a reader, not a value: once it is fully read, a second read returns
  empty, without an error.
- This lesson measured the boundary from the sending side; the full distribution of the
  course's thirteen boundary crossings is not yet built here, the next lesson builds it.

#### Next Step

Both lessons measured the boundary's **carrier** — the request and the response carry bytes
and text; the body can be read once. In neither of these two lessons did the body's
**content** matter at all; any random text in place of `"qty=3"` or `"qty=7 unit=kg"`
would have given the same results, because what was measured was the body's behavior, not
the data it carried. But neither has yet asked this question: when the sending side sends a
**struct**, what does it leave behind for the other side to rebuild it? The next lesson
measures this question with JSON and produces the course's binding number: a struct's field
names are carried across with struct tags, but this carrying has limits — a field with no
counterpart on the receiving side drops silently, and "it got rebuilt" appearing to be the
case does not always mean it was "rebuilt correctly."

### JSON Serialization

Source: https://academia.sh/en/courses/go-applications/json-serialization

The course's binding number is produced here: of the thirteen boundary crossings, type information is preserved in 1, rebuilt in 5, lost in 7. The schema that carries a struct's field names across is the struct tag; the same byte sequence decoded into an untyped container turns an integer into `float64`, a field with no counterpart on the receiver drops silently, and no error surfaces. The limiting measure: appearing to be rebuilt does not mean it was rebuilt correctly.

The previous lesson measured that a body crossing the boundary carries only bytes, no type
information at all. But that body's **content** was still a question mark — how is a piece
of text like `"qty=7 unit=kg"` interpreted by the requesting side? This lesson takes on
that question directly: if the sending side holds a **struct** in hand, which part of that
struct survives on the receiving side after crossing the boundary? The answer lies not in
the struct itself, but in a **schema** carried along with it — in Go, the name of this
schema is the struct tag.

This lesson also produces the course's binding measurement. The question the course asks is
the same in all twelve lessons: what happens to a value's type information as it crosses a
boundary? So far this question has been asked in two narrow examples (request parts,
response body). This lesson asks the question **at scale** — it runs thirteen network,
template, database, and command-line boundaries one after another in a single measurement
and writes each one into one of three outcomes: **preserved**, **rebuilt**, **lost**. Four of
these thirteen crossings are directly JSON's subject; the remaining nine are each deepened
separately in the rest of the course's lessons, and here they are only classified.

There is a reason this broad measurement is gathered into a single measurement: measuring
the thirteen crossings separately across thirteen lessons would lead each lesson to produce
its own small number, and no one would be able to see what these small numbers add up to
together. Gathering them in a single run lets the course's main claim (that the boundary
mostly loses type information) be proven with a single number. This lesson produces that
number; the remaining eleven lessons each expand one part of that number in turn.

#### Struct Tags: The Schema for Field Mapping

A Go struct's field names (`Name`, `Quantity`) are information that exists at compile time
and is also reachable at run time through reflection (`reflect`). But these names start with
a capital letter and follow Go's own naming rule — that is the requirement for a field to be
exported. There is no such rule for JSON's key names; the program on the other side of the
boundary might expect `"name"` in lowercase, or even an entirely different word. The struct
tag (`json:"name"`) carries the schema for the **field mapping** between these two: it says
which Go field corresponds to which JSON key. If there is no tag, the `encoding/json`
package uses the field name as it is; this lesson runs tagged and untagged fields side by
side.

The source below builds a shared helper that counts and prints this measurement's three
outcomes.

```go
// boundary.go — what happens to a value's type information when it crosses a boundary: is it preserved, rebuilt, or lost
package main

import "fmt"

var preserved, rebuilt, lost int

func observe(crossing, outcome, evidence string) {
	switch outcome {
	case "preserved":
		preserved++
	case "rebuilt":
		rebuilt++
	default:
		lost++
	}
	fmt.Printf("%-36s %-10s %s\n", crossing, outcome, evidence)
}

func summary() {
	fmt.Printf("boundary: %d preserved, %d rebuilt, %d lost\n",
		preserved, rebuilt, lost)
}
```

The measurement itself asks the thirteen boundary crossings in order. The first four are
directly JSON's subject: a round trip to the same struct, a field hidden by a tag, decoding
into an untyped container, and a field with no counterpart on the receiver. The remaining
nine (time, template, reader, network, shared schema, driver, command line) are each
deepened separately in the course's later lessons — which one is handled where is written
out one by one below; here they are only written into the same three outcomes.

```go
// measurement.go — thirteen boundary crossings, split into three outcomes
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"sort"
	"strings"
	"text/template"
	"time"
)

type Record struct {
	Name      string    `json:"name"`
	Quantity  int       `json:"quantity"`
	Timestamp time.Time `json:"timestamp"`
	Hidden    string    `json:"-"`
}

// Driver models the type set a database driver carries: only four types.
type Driver struct{ row []any }

func (d Driver) Read(i int) any { return d.row[i] }

func main() {
	fixedTime, _ := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")
	record := Record{Name: "bolt", Quantity: 12, Timestamp: fixedTime, Hidden: "invisible"}

	fmt.Println("-- what happens to type information at the boundary --")

	// 1-2. JSON round trip: struct -> text -> struct
	data, _ := json.Marshal(record)
	var rebuiltRecord Record
	json.Unmarshal(data, &rebuiltRecord)
	observe("JSON round trip (same struct)", "rebuilt",
		fmt.Sprint("quantity type=", fmt.Sprintf("%T", rebuiltRecord.Quantity), " equal=", rebuiltRecord.Quantity == record.Quantity))
	observe("untagged field (json:\"-\")", "lost",
		fmt.Sprint("did the hidden field come back=", rebuiltRecord.Hidden != ""))

	// 3. JSON -> map: no type schema
	var untyped map[string]any
	json.Unmarshal(data, &untyped)
	observe("JSON round trip (untyped container)", "lost",
		fmt.Sprint("quantity type=", fmt.Sprintf("%T", untyped["quantity"])))

	// 4. Unknown field drops silently
	var partial Record
	json.Unmarshal([]byte(`{"name":"screw","unknown":5}`), &partial)
	observe("field sent, missing on receiver", "lost",
		fmt.Sprint("name=", partial.Name, " error=false"))

	// 5. Time: type -> text -> type
	text := fixedTime.Format(time.RFC3339)
	again, _ := time.Parse(time.RFC3339, text)
	observe("time value to text and back", "rebuilt",
		fmt.Sprint("equal=", again.Equal(fixedTime), " type=", fmt.Sprintf("%T", again)))

	// 6. Template: output is text only
	tmpl := template.Must(template.New("t").Parse("{{.Name}} x{{.Quantity}}"))
	var output bytes.Buffer
	tmpl.Execute(&output, record)
	observe("template output", "lost", fmt.Sprintf("%q (%T)", output.String(), output.String()))

	// 7. Reader/writer: byte level
	content, _ := io.ReadAll(strings.NewReader("bolt"))
	observe("content from a reader", "lost", fmt.Sprintf("%T length=%d", content, len(content)))

	// 8-9. HTTP: body is bytes, the only shared hint is the content type
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(record)
	}))
	defer server.Close()
	response, _ := http.Get(server.URL)
	body, _ := io.ReadAll(response.Body)
	response.Body.Close()
	observe("HTTP body", "lost", fmt.Sprintf("%T length>0=%t", body, len(body) > 0))
	var received Record
	json.Unmarshal(body, &received)
	observe("resolving with the content type hint", "rebuilt",
		fmt.Sprint("content type=", response.Header.Get("Content-Type"), " quantity=", received.Quantity))

	// 10. Contract-based crossing: both sides share the same schema
	schema := []string{"name:string", "quantity:int"}
	sent := encode(schema, record)
	field, kind := decode(schema, sent)
	observe("shared schema crossing", "preserved", fmt.Sprint(field, " ", kind))

	// 11. Driver's type set: narrows to four types, scan rebuilds it
	d := Driver{row: []any{"bolt", int64(12)}}
	observe("driver's carried type set", "lost",
		fmt.Sprintf("quantity comes in as %T", d.Read(1)))
	var quantity int
	scan(d.Read(1), &quantity)
	observe("rebuild with scan", "rebuilt", fmt.Sprint("quantity=", quantity, " type=", fmt.Sprintf("%T", quantity)))

	// 12. Command line: arguments are strings, the parser rebuilds the type
	flagSet := flag.NewFlagSet("measurement", flag.ContinueOnError)
	flagSet.SetOutput(io.Discard)
	count := flagSet.Int("quantity", 0, "quantity")
	flagSet.Parse([]string{"-quantity", "12"})
	observe("command-line argument", "rebuilt", fmt.Sprint("quantity=", *count, " type=", fmt.Sprintf("%T", *count)))

	summary()
}

// encode and decode work with a schema shared by both sides: the type name stays in the schema.
func encode(schema []string, k Record) []string {
	return []string{k.Name, fmt.Sprint(k.Quantity)}
}

func decode(schema []string, fields []string) (string, string) {
	kinds := make([]string, 0, len(schema))
	for _, t := range schema {
		kinds = append(kinds, strings.SplitN(t, ":", 2)[1])
	}
	sort.Strings(kinds)
	return fields[0], strings.Join(kinds, "+")
}

// scan narrows the wide type the driver returns down to the target's type.
func scan(source any, target *int) {
	if v, ok := source.(int64); ok {
		*target = int(v)
	}
}
```

```
-- what happens to type information at the boundary --
JSON round trip (same struct)        rebuilt    quantity type=int equal=true
untagged field (json:"-")            lost       did the hidden field come back=false
JSON round trip (untyped container)  lost       quantity type=float64
field sent, missing on receiver      lost       name=screw error=false
time value to text and back          rebuilt    equal=true type=time.Time
template output                      lost       "bolt x12" (string)
content from a reader                lost       []uint8 length=4
HTTP body                            lost       []uint8 length>0=true
resolving with the content type hint rebuilt    content type=application/json quantity=12
shared schema crossing               preserved  bolt int+string
driver's carried type set            lost       quantity comes in as int64
rebuild with scan                    rebuilt    quantity=12 type=int
command-line argument                rebuilt    quantity=12 type=int
boundary: 1 preserved, 5 rebuilt, 7 lost
```

**ND14.** JSON's own share is the first four lines. First, on the round trip to the same
`Record` struct, the `Quantity` field comes back with its `int` type and value preserved —
in the `rebuilt` class, because what rebuilds the type is not the struct itself but the
sending and receiving side sharing **the same Go type**. Second, the `Hidden` field is never
sent at all because of the `json:"-"` tag: since it never enters the byte sequence, it does
not come back either — in the `lost` class, but the reason is not a loss, it is a deliberate
exclusion. Third and fourth arise from the same byte sequence (the `data` variable) being
decoded in two different forms.

**ND15.** The third line is this lesson's most striking observation: when the same `data`
byte sequence is decoded into a `map[string]any` variable, the `Quantity` field's source
value `12`, an `int`, comes back with a type of `float64`. The reason is that the JSON
format itself has no distinction between an integer and a decimal number — when the
`encoding/json` package is given no target type (`any` or `map[string]any`), it decodes
every number to `float64` by default. The struct tag plays no role here, because the target
itself is not a struct; the type schema only works when the target is a struct. The fourth
line measures the exact opposite situation: the byte sequence sent contains an `"unknown"`
key with no counterpart in the `Record` struct, and `json.Unmarshal` silently ignores it —
it returns no error, prints no warning, the line `name=screw error=false` confirms this.

**ND16.** What these four lines have in common is that the `encoding/json` package has a
**forgiving** design: a missing field, an extra field, a target with no type schema — none
of them produce an error. This forgiveness sharpens the lesson's opening claim: an operation
that looks "rebuilt" might not have been built correctly. The call
`json.Unmarshal([]byte(...), &partial)` returns a `nil` error; the code looks like it fails
nowhere — but there is no trace, anywhere in the `partial` variable, of the `"unknown"`
field that was sent. The only way for a program to notice this silence is to **manually
compare** what was sent against what was received; neither the compiler nor the runtime
reports this difference on its own.

#### The Remaining Nine Crossings: The Course's Full Scale

The section from the measurement's fifth line to its thirteenth line is the subject of the
course's remaining lessons. The fifth line measures a time value turning into text and
coming back — the Time Operations lesson of the System and Data Access topic deepens this.
The sixth line measures that a template's output is text only — the next lesson takes on
this line by itself. The seventh and eighth lines measure the reader/writer abstraction and
the HTTP body; both were or will be handled separately in the previous two lessons and in
the System topic's first lesson. The ninth line shows how the content type header is used as
a hint — the one-line counterpart, here, of the subject measured in detail in the first
lesson.

The tenth line is the course's **only** `preserved` line: the `encode` and `decode`
functions read the same `schema` slice, so no ambiguity remains between the field names sent
and the field names decoded. This line is deepened in the Contract-Based Service
Communication lesson — there, how the schema is **distributed** to both sides is measured
separately, because in this measurement the schema already sits in the same source file on
both sides, and the distribution problem is not modeled. The eleventh and twelfth lines
measure a database driver's narrow type set and how that set is rebuilt with `scan` — the
subject of the Database Access lesson. The thirteenth line measures how a command-line flag
is rebuilt from a string to its target type — the subject of the Command-Line Applications
lesson.

**ND17.** None of these nine lines is handled in detail here; each will be taken up again in
its own lesson, with its own measurement and its own limiting observation. Here, the only
function of these lines is to **complete** the course's binding number: JSON's four lines (0
preserved, 1 rebuilt, 3 lost) do not give the course's full picture on their own, because
the course's only example of the `preserved` class is outside JSON, in the shared-schema
crossing. The total that emerges when all thirteen lines run together — 1 preserved, 5
rebuilt, 7 lost — is used as a fixed reference for the rest of the course from this lesson
onward.

Another reason these nine lines stand here in the measurement is that the examples depend
on each other: the eighth and ninth lines (`HTTP body`, `resolving with the content type
hint`) use the same `record` variable, so they share the same data as JSON's four lines.
This sharing is not accidental — because the course's twelve lessons are separate views of
the same event (a value crossing a boundary), passing the same sample data through different
boundaries demonstrates the course's consistency. Each of the following lessons will build
new data specific to its own subject and do its own measurement; this measurement only
proves that all of them sit within **the same frame**.

**ND18.** Reading this distribution: **more than half** of the thirteen crossings (seven of
them) lose type information entirely, five are rebuilt with a schema held by the other side,
and only one never loses it at all, thanks to both sides sharing the same schema. This ratio
reflects the standard library's design philosophy: the far side of a boundary is **untyped**
by default, and type only comes back with an explicitly built schema. No crossing carries
type on its own, automatically.

What the five `rebuilt` lines have in common is also instructive: in each one, a different
mechanism rebuilds the type — sharing the same Go type (the first line), a parsing rule
(`time.Parse`), a content type hint, a scanning function (`scan`), a flag parser. The only
thing they share is that all of them are **explicitly written in the other side's code**;
type never appears on its own anywhere, someone has to have written a function that rebuilds
it. This is a pattern that will be encountered again in every lesson of the course:
rebuilding is not a law of nature, it is a line of code.

#### Summary

- A struct tag (`json:"name"`) carries the schema for the field mapping between a Go
  field's name and a JSON key's name; if there is no tag, the field name is used as it is.
- A field marked with the `json:"-"` tag never enters the byte sequence; since it never
  crosses the boundary at all, it does not come back either.
- When the same byte sequence is decoded not into a struct but into a container with no
  type schema (`map[string]any`), integer values come back as `float64`.
- A field present on the sending side with no counterpart in the receiving struct drops
  silently; `json.Unmarshal` returns no error in this case.
- The course's full distribution of thirteen boundary crossings was built in this lesson: 1
  preserved, 5 rebuilt, 7 lost; these numbers are a binding reference for the rest of the
  course.
- The single `preserved` line is the crossing where both sides share the same schema; in
  none of the remaining twelve lines do both sides have the same schema.

#### Next Step

This lesson measured that a struct tag is a **schema** that carries a field name across, and
that without a schema there is no rebuilding either. The course's binding number was also
built here: type information is lost entirely in seven of the thirteen crossings, rebuilt
with a schema in five, and never lost at all in one — these three numbers will each grow
individually over the next nine lessons. The next topic looks at an exit where no schema
exists at all: a template produces only text from the struct it receives, and no trace
remains in that text of which field came from where — this measurement's sixth line
summarized that in one sentence, the next lesson takes up that same line on its own, in
detail.

### Templates

Source: https://academia.sh/en/courses/go-applications/templates

The same template text, run with two different Go types (an integer, a decimal number), produces byte-for-byte identical output; the source's type information is lost entirely at the output. A field name appears in the output only if it was written into the template's own text — it does not come from the type system. Text and HTML templates turn the same data into different byte sequences with respect to escaping. The limiting measure: a template looks up field names at run time, a nonexistent field only becomes known at execution, not at compile time, and the error is born in the middle of the output.

The previous lesson's last measurement showed what a struct tag is for: a struct tag was a
schema that carried a field name to the other side of the boundary, and without that schema
there was no rebuilding either. This lesson asks the same question in reverse. JSON
serialization turned a struct into bytes and brought it back — there was a schema between
the outbound trip and the return, and the schema rebuilt the type of five of the four fields.
Templates never promise this return at all: what a template produces is only text, and that
text is assumed never to be read again with any schema. The course's question stays the
same — what happens to a value's type information as it crosses a boundary? — but this time
the other side of the boundary is not a program, it is a screen or a file.

Go's standard library carries two template packages: `text/template` and `html/template`.
Both use the same template language — `{{.Field}}` reads a field, `{{if}}` and `{{range}}`
provide branching and iteration — and both take a Go value and write text to an
`io.Writer`. The only difference between them is the escaping behavior measured shortly.
Both are called by their package name in the code blocks; in the prose they are referred to
only as "the text template" and "the HTML template."

The `Execute` method's second parameter is an `io.Writer` — this lesson uses a buffer
(`bytes.Buffer`), but the same call could just as well write to a file, standard output, or
a network connection; the System and Data Access topic's first lesson will separately
measure that the same interface is shared between a file and a buffer. The template package
itself does not know, and does not need to know, what the destination is — it only produces
and writes a sequence of bytes. This lesson measures exactly what does and does not stand
inside these produced bytes: as the source data crosses the boundary, does the type system
stay on Go's own side, or does it carry through to the text the template produces?

The course's shared measurement split the thirteen boundary crossings into three classes:
type is preserved in one, rebuilt in five, lost in seven (the full distribution built in
Lesson 03). Template output is one of these seven lost crossings — but the **shape** of the
loss differs from JSON's loss, because JSON is a round-trip format (converted to bytes, then
readable back), while a template never promises a return at all; it is not even assumed that
the text it produces will ever be parsed again.

#### A Template's Only Output Type: Text

**ND19.** What a template's `Execute` call produces, no matter what the source data's type
is, is always a byte sequence — and once that byte sequence is turned into a string, it
carries none of the source's type information. The most direct way to show this is to run
the same template text with two different Go types and compare the outputs.

```go
// templates.go — measures the type of a template's output, field traces, the escaping difference, and run-time lookup
package main

import (
	"bytes"
	"fmt"
	"html/template"
	"strings"
	textTemplate "text/template"
)

type Product struct {
	Name     string
	Quantity int
	Comment  string
}

type ProductInt struct{ Quantity int }
type ProductFloat struct{ Quantity float64 }

func main() {
	product := Product{Name: "bolt", Quantity: 12, Comment: "good <b>but</b> expensive & slow"}

	fixedTemplate := textTemplate.Must(textTemplate.New("t").Parse("{{.Quantity}}"))
	var bufFromInt, bufFromFloat bytes.Buffer
	fixedTemplate.Execute(&bufFromInt, ProductInt{Quantity: 12})
	fixedTemplate.Execute(&bufFromFloat, ProductFloat{Quantity: 12})
	fmt.Println("-- same template, two different source types --")
	fmt.Println("output from the int:", bufFromInt.String())
	fmt.Println("output from the float:", bufFromFloat.String())
	fmt.Println("are the two outputs byte-for-byte the same:", bufFromInt.String() == bufFromFloat.String())

	narrow := textTemplate.Must(textTemplate.New("narrow").Parse("{{.Name}} x{{.Quantity}}"))
	var bufNarrow bytes.Buffer
	narrow.Execute(&bufNarrow, product)
	fmt.Println()
	fmt.Println("-- a field name appears in the output only if the template's own text wrote it --")
	fmt.Println("narrow template output:", bufNarrow.String())
	fmt.Println("does \"Name\" appear in the output:", strings.Contains(bufNarrow.String(), "Name"))
	fmt.Println("does \"Quantity\" appear in the output:", strings.Contains(bufNarrow.String(), "Quantity"))

	wide := textTemplate.Must(textTemplate.New("wide").Parse("Name: {{.Name}}, Quantity: {{.Quantity}}"))
	var bufWide bytes.Buffer
	wide.Execute(&bufWide, product)
	fmt.Println("wide template output:", bufWide.String())
	fmt.Println("does \"Name\" appear in the output:", strings.Contains(bufWide.String(), "Name"))

	textTmpl := textTemplate.Must(textTemplate.New("m").Parse("{{.Comment}}"))
	var bufText bytes.Buffer
	textTmpl.Execute(&bufText, product)
	htmlTmpl := template.Must(template.New("h").Parse("{{.Comment}}"))
	var bufHtml bytes.Buffer
	htmlTmpl.Execute(&bufHtml, product)
	fmt.Println()
	fmt.Println("-- the same data produces different bytes from a text and an html template --")
	fmt.Printf("text template: %s\n", bufText.String())
	fmt.Printf("html template: %s\n", bufHtml.String())
	fmt.Println("are the two outputs the same:", bufText.String() == bufHtml.String())

	broken := textTemplate.Must(textTemplate.New("broken").Parse("before: {{.Name}}, after: {{.MissingField}}"))
	var bufBroken bytes.Buffer
	err := broken.Execute(&bufBroken, product)
	fmt.Println()
	fmt.Println("-- a nonexistent field errors at run time, the part written before it stays written --")
	fmt.Println("did an error occur:", err != nil)
	fmt.Printf("part that reached the output before the error: %q\n", bufBroken.String())
}
```

```
-- same template, two different source types --
output from the int: 12
output from the float: 12
are the two outputs byte-for-byte the same: true

-- a field name appears in the output only if the template's own text wrote it --
narrow template output: bolt x12
does "Name" appear in the output: false
does "Quantity" appear in the output: false
wide template output: Name: bolt, Quantity: 12
does "Name" appear in the output: true

-- the same data produces different bytes from a text and an html template --
text template: good <b>but</b> expensive & slow
html template: good &lt;b&gt;but&lt;/b&gt; expensive &amp; slow
are the two outputs the same: false

-- a nonexistent field errors at run time, the part written before it stays written --
did an error occur: true
part that reached the output before the error: "before: bolt, after: "
```

**ND20.** `ProductInt.Quantity` is an `int`, `ProductFloat.Quantity` is a `float64` — when
both are run through the same `{{.Quantity}}` template, the output is the string `12` in
both cases, byte-for-byte equal. While the template was running it knew what type the source
came from (`Execute` takes an interface value and reaches its field through reflection), but
no trace of this information remains in the text it produces. A program reading the output
cannot work backward from the string `12` to answer the question "was this an integer or a
decimal?"

This lands on the same surface as the previous lesson's untyped-container observation, but
by a different route. When a JSON value was decoded into an untyped container, a number
always became `float64` — there was a loss, but the loss had **a specific direction**: every
number fell into the same single type, and that fall was `encoding/json`'s own rule.
Template output does not even have a rule like this, because there is no target type at all
— `int` and `float64` both fall to the same `string`, but this fall has no structure like
"converging on a single target type"; both became ordinary text either way, and the text itself
belongs to no type. JSON's loss was a **converter's** decision; the template's loss is the
result of there being **no target type at all** to convert to.

**ND21.** In the `bolt x12` text the narrow template (`{{.Name}} x{{.Quantity}}`) produces,
neither the word `"Name"` nor `"Quantity"` appears — field names are lost just as much as
type information, because what the template writes is the field's **value**, not the
field's **name**. `"Name"` does appear in the wide template's output (`Name: {{.Name}},
Quantity: {{.Quantity}}`), but not because Go automatically carries the struct field's name
along — the person who wrote the template typed the string `"Name: "` into the template by
hand, as plain text. A field name appearing in the output is not a guarantee coming from the
type system, it is a decision made by whoever wrote the template text.

A template's output being pure text carries a flexibility as much as it carries a
limitation: the destination does not have to be an HTML document. `text/template` can also
be used to produce a configuration file, an email body, a log line, even a source file
written in another language — whatever is produced, it makes no difference to the template
package's eyes, because the package only translates one piece of text into another,
substituting field values in place. The course's next lesson picks up exactly this last use,
source code generation, from precisely this point.

`ProductInt` and `ProductFloat` were deliberately defined as two parallel, single-field
types: the only difference between them is the type of the `Quantity` field, nothing else
changes. This makes clear that what the measurement shows is only "type information is
lost," not something caused by another structural difference (field count, field order,
field name) — the two types respond to the same template action (`{{.Quantity}}`) under the
same name, and because type is the only variable between them, the equality in the output
can be attributed directly to that single variable.

#### The Escaping Difference Between Text and HTML Templates

**ND22.** The same `Product` value, with the same `{{.Comment}}` template text, produces two
different byte sequences when given to two different packages. `text/template` writes the
`<b>` and `&` characters in the `Comment` field exactly as they are, without touching them at
all. `html/template` turns the same characters into the strings `&lt;`, `&gt;`, and `&amp;`
— this transformation is **escaping** (the term established in the M14/K01 Foundations of
the Web and HTML course). `html/template` does this automatically because it assumes its
output will be placed inside an HTML document, and knows that the characters `<`, `>`, `&`
have a special meaning in HTML; `text/template` makes no such assumption, because it does
not know where its output is going.

The M14/K01 course built HTML generation and escaping behavior on the browser side, in the
context of processing a document arriving from a server; that is not repeated here, the only
difference is that these two packages stand inside the same standard library, as a
production step on the server side. The choice between the two packages is not a matter of
style: if the output is going into an HTML page, `html/template` must be used, because a
text template that skips escaping produces an output that writes a string like `<script>` in
the data exactly as it is, if one appears.

`html/template`'s escaping works in more detail than it appears in this measurement: the
package detects **where** in the template a field is used (inside a tag body, an attribute
value, a link address) during parsing, and applies the escaping rule appropriate to each
context — a quotation mark inside an attribute value and a `<` character inside a tag body
are escaped by different rules. This lesson only measures the simplest context (a tag body);
context awareness itself is a separate subject and is not deepened here. The important
result is this: `text/template` has no idea in what context its output will be used and
changes no characters at all, while `html/template` assumes its output will be placed into
an HTML document and behaves accordingly. When unescaped user input is placed as it is
inside an HTML document, the browser may read it not as plain text but as **structure** —
escaping itself is a step that changes not the appearance of the value passing through, but
how the other side interprets it.

#### A Field Name Is Looked Up at Run Time

**ND23.** The expression `{{.MissingField}}` asks for a field that does not exist on the
`Product` type. This template compiles — because the template text is a string written in a
separate language Go's compiler never sees; the compiler knows which fields the `Product`
type has but never reads the expression `{{.MissingField}}` inside a string. The field
lookup happens through reflection **at run time**, when `Execute` is called, and when the
field cannot be found at that moment, `Execute` returns an error. A question similar to the
course's "the counterpart of what was never written" (the axis established in M09/K01, not
reused here) also holds here: a layer the compiler never looks at produces its own error on
its own schedule.

A template's own error moments are not singular either. The `template.Must` call wraps the
error `Parse` returns and panics — this never triggered throughout this lesson, because
every template's text was syntactically valid (every `{{` always closed with a `}}`, every
action was a recognized action). A `Parse` error, for an unclosed `{{` fragment for example,
comes out right at the start of the program, while the template is being built. An `Execute`
error, on the other hand, comes out only when the requested field is missing, while the
syntax is entirely valid. Both happen at run time (Go's compiler sees neither), but one
surfaces the moment the template is built, the other only when it is actually run with real
data — and this lesson only measured the second one, because no example that would trigger
the first was used.

**ND24.** The `broken` template carries the text `"before: {{.Name}}, after:
{{.MissingField}}"` and contains two separate actions: first `{{.Name}}` evaluates
successfully and the text `"before: bolt, after: "` is written to the `bufBroken` buffer;
then `{{.MissingField}}` fails and `Execute` returns an error. But the buffer is not rolled
back — the part written before the error stays where it is, because `Execute` writes text
**progressively**, it does not accumulate a result and write it all at once at the end. This
explains where the string `"before: bolt, after: "` seen in the measurement's last line
comes from: the error is born in the middle of the output, while the second field is being
looked up, and everything produced up to that point has already reached the `io.Writer`.

**ND25.** This adds a new angle to the course's distinction between "rejected" and "left
exposed." The compiler never sees the field name error — the source compiles under every
condition — and the error surfaces neither at compile time nor while the template is being
parsed (in the `Parse` call); only when `Execute` runs, and the output written at that
moment of execution becomes permanent. The only way to test a template before putting it
into production is to **run** it with real data; a tool that statically checks the
correctness of field names falls outside this pair.

The practical consequence of this is that a rarely triggered branch inside a template (a
line that only passes through under a `{{if}}` condition with specific data) can go months
without ever running at all. The Go Command Family lesson of the Go Fundamentals course
showed that the vet tool catches a pattern the compiler misses — a mismatch between a format
string and an argument's type. There is no second checker like this for template field
names; the template language itself lives outside Go's type system, in a separate language,
and runs in a layer neither the compiler nor the vet tool can reach. This is why a template's
correctness is measured not by the assurance compilation gives, but by the assurance of
having run that template with real data at least once — and this lesson did exactly that, by
running it.

#### Summary

- What a template produces is always text; no matter what the source's type is (`int`,
  `float64`), the same template produces the same string, and there is no way to work
  backward from the output to the source's type.
- Field names do not automatically appear in the output either — if a name appears in the
  output, it is because the person who wrote the template typed that name into the template
  as plain text.
- `text/template` and `html/template` share the same template language, but `html/template`
  runs its output through HTML escaping; the same data produces two different byte sequences
  in the two packages.
- A field name in a template is looked up through reflection not at compile time but while
  `Execute` runs; a nonexistent field halts not the template's compilation but its
  execution.
- The error is born in the middle of the output: the part `Execute` wrote before returning
  the error stays permanently in the `io.Writer`, it is not rolled back.

#### Next Step

This lesson measured that a template's output is pure text carrying no type — and that no
schema had been set up on the other side to read this text. The difference between this
lesson and the course's previous lesson (JSON serialization) stands exactly here: one is a
trip out that returns with a schema, the other is an exit that never returns at all. But the
two lessons share a common point: in both, the schema, where it existed, stood on only
**one side** of the boundary — the target type on the receiver in JSON, nobody at all in the
template. The next lesson joins these two ends: when two separate sides share the same
schema, what happens to the type information crossing the boundary? Contract-based service
communication answers this question by building the schema itself in a form that generates
both sides' source — and there it builds the single line, among the course's thirteen
boundary crossings, where type information is preserved.

### Contract-Based Service Communication

Source: https://academia.sh/en/courses/go-applications/contract-based-service-communication

The source of two independent processes is generated with text/template from the same shared schema (a list of field names, field types, JSON tags) and compiled and run separately. While the schema is shared, all three fields preserve their type — the single line, among the course's thirteen boundary crossings, that falls into the preserved class. Once the schema diverges (if the receiving side's generator is out of date), the crossing breaks silently: two of the three fields preserve their type, one is lost without ever producing an error.

The previous lesson showed that the text a template produces is not expected to be read
back by anyone: the output was only text, and there was no side that would read that text
back and bind it to a type. This lesson starts from the exact opposite end. Two independent
programs (two separate processes, two separate compilations) will pass a value between them,
and this time the same schema will stand on **both sides** of the boundary. The course's
question does not change — what happens to a value's type information as it crosses a
boundary? — but this lesson builds the single line, among the thirteen boundary crossings,
that falls into the **preserved** class.

A schema here is a description both sides agree on over a list of fields: each field's name,
Go type, and JSON tag. In earlier lessons the schema always stood on **one** side of the
boundary — the target type in the receiver's hand for JSON, the rule the vet tool knew in
the command family. In this lesson the schema is held in a third place belonging to neither
side of the boundary (as a Go slice, in the program's own memory), and **the same schema**
is used to generate both sides' source. The generation is done with the standard library's
own template package; no third-party tool, protocol, or data format comes into play.

It would also have been possible to write both sides' source by hand, separately — two
`Record` types, in two files, with the same field names. But in that case being "the same"
would turn into a matter of **discipline**: one could change while the other was forgotten,
and this forgetting would not be visible in the source code itself. Holding the schema in a
single place and generating two sources from it does not remove this risk — as will be seen
shortly, if the generator itself is not updated, the same problem comes back — but it
gathers the risk into **a single point**: it is no longer two hand-written sources staying
in sync that matters, it is enough for a single schema to be fed into both generation calls
at the same time.

#### Generating Both Sides' Source from the Same Schema

**ND26.** The procedures below take a schema (a `FieldSchema` slice) and run a single
template that can produce two different sources depending on the `Role` field: the
`"sender"` role produces a program that turns a value into JSON and writes it to standard
output, the `"receiver"` role produces a program that reads from standard input and decodes
it into the same struct. `run` runs both generated sources, each in **a separate temporary
module**, with `go run .`, as a separate process — these two "sides" never see each other's
source at all, they exchange bytes only through standard input and output.

```go
// contract.go — the procedures that run a shared schema to generate both sides' source
package main

import (
	"bytes"
	"os"
	"os/exec"
	"path/filepath"
	"text/template"
)

type FieldSchema struct {
	Name    string
	Type    string
	Tag     string
	Example string
}

type TemplateData struct {
	Fields []FieldSchema
	Role   string
}

func generate(tmpl *template.Template, data TemplateData) string {
	var buf bytes.Buffer
	tmpl.Execute(&buf, data)
	return buf.String()
}

func run(source string, input []byte) string {
	dir, _ := os.MkdirTemp("", "contract")
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module side\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "main.go"), []byte(source), 0o644)

	cmd := exec.Command("go", "run", ".")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	if input != nil {
		cmd.Stdin = bytes.NewReader(input)
	}
	output, _ := cmd.Output()
	return string(output)
}
```

`generate` writes the template into a `bytes.Buffer` and returns it as a string — exactly
the step the previous lesson measured, used here to produce a piece of source code. `run`
is the same pattern established in the Go Fundamentals course's Go Command Family lesson:
writing the source to a temporary module and calling the compiler (here through `go run`)
from inside the program. The difference is here — there, a single source was being formatted
or checked; here, two different sources, both derived from the same schema, run in two
separate processes unaware of each other.

The template itself stands in a second block; the template text produces one struct field
for every field in the schema slice, and depending on the role, either an encoding or a
decoding body. The import list also branches on role: `"fmt"` and `"io"` are only added in
the `receiver` role, because the `sender`'s body never uses either of them. This distinction
is not arbitrary — that an unused import stops compilation in Go was a rule established in
the Go Fundamentals course; the template does not know this rule, but the generator (that
is, this lesson) does, and while producing two different source texts, it ensures both stay
independently compilable with an `{{if}}` block that branches on role.

```go
// measurement.go — measuring the contract-based crossing with a shared schema and a diverged schema
package main

import (
	"fmt"
	"strings"
	"text/template"
)

const backtick = "`"

var templateText = `package main

import (
	"encoding/json"
{{if eq .Role "receiver"}}	"fmt"
	"io"
{{end}}	"os"
)

type Record struct {
{{range .Fields}}	{{.Name}} {{.Type}} ` + backtick + `json:"{{.Tag}}"` + backtick + `
{{end}}}

func main() {
{{if eq .Role "sender"}}	r := Record{
{{range .Fields}}		{{.Name}}: {{.Example}},
{{end}}	}
	data, _ := json.Marshal(r)
	os.Stdout.Write(data)
{{else}}	data, _ := io.ReadAll(os.Stdin)
	var r Record
	json.Unmarshal(data, &r)
{{range .Fields}}	fmt.Printf("%s type=%T value=%v\n", "{{.Name}}", r.{{.Name}}, r.{{.Name}})
{{end}}{{end}}}
`

func main() {
	tmpl := template.Must(template.New("side").Parse(templateText))

	sharedSchema := []FieldSchema{
		{Name: "Name", Type: "string", Tag: "name", Example: `"bolt"`},
		{Name: "Quantity", Type: "int", Tag: "quantity", Example: "12"},
		{Name: "Price", Type: "float64", Tag: "price", Example: "19.90"},
	}

	senderSource := generate(tmpl, TemplateData{Fields: sharedSchema, Role: "sender"})
	fmt.Println("-- generated sender source --")
	fmt.Println(senderSource)

	transferredBytes := run(senderSource, nil)

	receiverSource := generate(tmpl, TemplateData{Fields: sharedSchema, Role: "receiver"})
	receiverOutput := run(receiverSource, []byte(transferredBytes))
	fmt.Println("-- schema shared: the receiver reads all three fields --")
	fmt.Print(receiverOutput)
	sharedFieldCount := strings.Count(strings.TrimRight(receiverOutput, "\n"), "\n") + 1

	staleSchema := sharedSchema[:2]
	staleReceiverSource := generate(tmpl, TemplateData{Fields: staleSchema, Role: "receiver"})
	staleReceiverOutput := run(staleReceiverSource, []byte(transferredBytes))
	fmt.Println()
	fmt.Println("-- schema diverged: the receiver side's generator schema is stale --")
	fmt.Print(staleReceiverOutput)
	staleFieldCount := strings.Count(strings.TrimRight(staleReceiverOutput, "\n"), "\n") + 1

	fmt.Println()
	fmt.Printf("schema shared: %d/%d fields' types preserved\n", sharedFieldCount, len(sharedSchema))
	fmt.Printf("schema diverged: %d/%d fields' types preserved, %d field silently lost\n",
		staleFieldCount, len(sharedSchema), len(sharedSchema)-staleFieldCount)
}
```

```
-- generated sender source --
package main

import (
	"encoding/json"
	"os"
)

type Record struct {
	Name string `json:"name"`
	Quantity int `json:"quantity"`
	Price float64 `json:"price"`
}

func main() {
	r := Record{
		Name: "bolt",
		Quantity: 12,
		Price: 19.90,
	}
	data, _ := json.Marshal(r)
	os.Stdout.Write(data)
}

-- schema shared: the receiver reads all three fields --
Name type=string value=bolt
Quantity type=int value=12
Price type=float64 value=19.9

-- schema diverged: the receiver side's generator schema is stale --
Name type=string value=bolt
Quantity type=int value=12

schema shared: 3/3 fields' types preserved
schema diverged: 2/3 fields' types preserved, 1 field silently lost
```

**ND27.** `sharedSchema` is a three-element Go slice, defined exactly **once** inside the
`main` function. Both `senderSource` and `receiverSource` are generated from this **same
variable** — `sharedSchema` is passed to both `generate` calls, there is no other copy, no
other hand-written list. This satisfies the rule the BOUNDARY shared definition sets: if a
lesson is going to write a `preserved` line, it also has to show how the schema reached both
sides — the way shown here is that the same memory value becomes the input to two separate
generation calls. The generated sources are then completely independent of each other: they
are compiled and run as separate processes, in separate temporary directories, with separate
`go.mod` files. The `sender` side's source never sees the `receiver` side's source at all,
the two talk only through `transferredBytes`, as a plain byte sequence.

Running as separate processes is not a detail here, it is the body of the measurement. If
`sender` and `receiver` shared a **single** `Record` type imported from the same Go package,
type preservation would be the compiler's own guarantee — the two sides would already be
looking at the same memory layout, and this lesson would have assumed from the start the
very thing it wants to measure. Talking only through JSON bytes between two separately
compiled, separately running binaries is what makes the boundary a real boundary: the two
sides agree on the same type not because the schema is shared, but because **each of them
independently generated** the fact that the schema is shared.

**Blind spot.** This measurement's `preserved` class is built on the assumption that the
schema really reaches both sides — and this lesson **models** that reaching as two
generation calls reading the same Go slice. In production, both sides sharing the same
schema usually requires a distribution step (one side's updated schema somehow reaching the
other); this lesson does not measure that distribution step itself, it only measures what is
gained when distribution **succeeds** and what is lost when it **fails** (the section
below).

#### While the Schema Is Shared, All Three Fields Preserve Their Type

**ND28.** When the `receiver` source is generated from `sharedSchema`, it decodes the JSON
the `sender` wrote into its own `Record` type and reads all three fields with their correct
type: `Name` comes back as a `string`, `Quantity` as an `int`, `Price` as a `float64` —
identical to the types on the source side. This line is the course's single `preserved` line
in the full distribution of thirteen boundary crossings (1 preserved, 5 rebuilt, 7 lost; the
number built in Lesson 03). The `rebuilt` lines in earlier lessons also used a schema (a
struct tag, a target type, a stamp in a format string), but that schema always stood on
**one** side of the boundary, and the other side knew its own schema on its own, from a
separate source. Here both sides are the product of **the same** schema — this difference is
the entire distance between "rebuilt" and "preserved."

**ND29.** The sender's and receiver's source code differ from each other: one calls
`json.Marshal`, the other `json.Unmarshal`; one writes to `os.Stdout`, the other reads from
`os.Stdin`. But their `Record` types carry byte-for-byte identical field names and identical
JSON tags, because both ran the `{{range .Fields}}` loop over the **same** `sharedSchema`
slice. The code itself is not the same, but the **schema** that generated the code is — and
JSON's field mapping (built over a tag's string counterpart) works only as long as these two
tags are the same. Because the schema comes from a single source, the tags being the same is
not a coincidence, it is the direct result of how they were generated.

#### When the Schema Diverges: A Silent Loss

**ND30.** `staleSchema` is `sharedSchema`'s first two elements — there is no `Price` field.
This models a situation where the receiving side's generator was not updated while the
sending side stayed current: the two sides no longer share the same schema.
`staleReceiverSource` is generated from this incomplete schema, and its own `Record` type
has only the `Name` and `Quantity` fields. This source **compiles** — the absence of `Price`
is no problem at all in Go's type system, because a missing field never demands being part
of another type — and it **runs**, the `json.Unmarshal` call returns no error.

**ND31.** `transferredBytes` carries all three fields
(`{"name":"bolt","quantity":12,"price":19.9}`), but the struct `staleReceiverSource` decodes
into can only read two of them. The `price` key sits in the JSON text, `json.Unmarshal` sees
it, and because it has no counterpart in the target struct it **silently skips it** — the
exact same mechanism as the JSON lesson's observation, two shares back, that "a field with
no counterpart on the receiver drops silently," with a single difference here: the loss is
born not from a programming error but from the two sides' schemas **diverging from each
other**. The measurement's last line gives this in numbers: 3/3 while the schema is shared,
2/3 once the schema diverges, with no warning, no error at all about the lost field.

The importance of direction shows up here: the side that is missing is the **receiver**. As
long as the sending side's source stays current, that is, as long as it still sends `Price`,
no sign of the fault appears on the sending side at all — `senderSource` never changed in
this lesson; the `run(senderSource, nil)` call produced the same bytes in both scenarios. The
problem is only in the source the receiving side generates, and in the fact that the side
generating that source (the generator using its own `staleSchema`) was not updated. If the
roles were reversed — the sender generated with the stale schema, the receiver with the
current one — the result would differ: the `Price` field the receiver expects would not be
present in the JSON at all, and the struct would fill this field with its own zero value
(`0`), because `encoding/json` silently ignores a missing key exactly like it ignores an
unknown one. Both directions fall into the same class (lost), but **where** the loss appears
changes: in one direction the field is not in the struct the receiver reads at all, in the
other the field is there but always zero.

**ND32.** This proves the whole claim of contract-based communication in reverse: the
preservation does not come from the schema **itself**, it comes from the schema's
**distribution**. As long as the same schema reaches both sides at the same time, the
crossing stays in the preserved class; if one side is updated and the other is left behind,
the crossing falls back into the "lost" class without producing any error. A single shared
schema source (the `sharedSchema` variable in this lesson) is preservation's
**precondition**; the schema itself is never a guarantee, only a possibility that depends on
how in sync the two sides stay.

This result's relationship to the course's general claim is not indirect, it is direct: in
five of the thirteen boundary crossings type was "rebuilt," and in each of them the side
that rebuilt it did so from a schema in the other side's hand — but that schema was always
the other side's **own** schema, and there was no mechanism confirming it was the same as
the sending side's schema. This lesson revealed the difference between "rebuilding" and
"preservation" by reducing those two schemas to a single one: rebuilding rested on the other
side's **guess** turning out to be correct; preservation rests on there being no need for a
guess at all.

#### Summary

- The source of two independent processes was generated with `text/template` from the same
  shared schema slice and compiled and run separately in temporary modules; the sides never
  saw each other's source, they only exchanged bytes.
- While the schema is shared, all three fields preserve their type: `Name`, `Quantity`,
  `Price` — the types on the source and target sides matched exactly. This is the single
  line, among the course's thirteen boundary crossings, that falls into the `preserved`
  class.
- Preservation was shown by passing the schema into the `generate` call from **the same
  value**; the rule that a "preserved" line cannot be written without evidence was fully
  satisfied here.
- When the schema diverges on the two sides (a field is generated missing), the source
  still compiles, the program still runs, but the missing field drops without producing any
  error — a silent loss from 3/3 to 2/3.
- Contract-based communication's preservation is not a guarantee, it is an outcome that
  depends on the two sides' schemas staying in sync.

#### Next Step

The sharing measured in this lesson was not about what the schema was, but about **when**
it was set up: the sending side's source was generated and run, then the receiving side's
source started in a separate process and read the byte the sender had left behind — the two
sides never ran at the same time, and there was a single exchange between them: a write that
looks like a request, a read that looks like a response. The next lesson removes this
assumption: when a connection **lasts a long time**, when many crossings flow one after
another over the same connection instead of a single exchange, and when one side can
**push** data without a request coming from the other side, what changes about the boundary?

Framing comes into play as the single piece of information that carries where a message
starts and ends — the single mechanism that says where a stream ends one message and begins
the next. But this lesson has shown exactly that drawing a boundary correctly is not the
same thing as knowing the type of the value inside it: framing says **where** a message
ends, not whether fields like `Name`, `Quantity`, `Price` stand inside that message.
Answering that question will again require a schema — a schema of the kind this lesson
measured.

### Real-Time Communication

Source: https://academia.sh/en/courses/go-applications/real-time-communication

A long-lived connection is modeled, without opening a real port, with a byte buffer implementing io.Reader and io.Writer. The server pushes three messages one after another, without waiting for a request between them. In an unframed stream, the three pushes merge into a single byte sequence and fixed-size reads cannot correctly separate any message (0/3); in a stream framed with a length prefix, all three parse back correctly (3/3). The limiting measure: framing gives where a message ends, not the type of the value inside it.

The previous lesson measured a boundary where the two sides make a single exchange: the
sender wrote once, the receiver read once, the two sides never ran at the same time. This
lesson removes that assumption. When a connection **lasts a long time** — carrying many
crossings one after another without opening and closing — the only thing crossing the
boundary is no longer a byte sequence's **content**, it is **where** that byte sequence
ends. The course's question stays the same, but this time it is asked not at a single
value's boundary, but at a stream stretching out over time.

A real network connection is never opened in this lesson. Instead, a `bytes.Buffer` is used
— the standard library's byte buffer, implementing both the `io.Reader` and `io.Writer`
interfaces, exactly as a real network connection would. This lesson **models** the
connection with this buffer; the code being measured uses only these two interfaces, and the
same code would run unchanged over a real connection too, because both satisfy the same
contract. No address, port, or duration is printed anywhere, because none of them exist at
all in this model.

This continues the strategy followed by the course's HTTP Server and HTTP Client lessons.
Both of those lessons measured with a test helper, without opening a real port; here the
same idea is applied at a different layer. HTTP's own test helper was enough to model a
single request–response pair, because HTTP's boundary was already clear — a request starts,
a response ends, the connection either closes or waits ready for the next request. A
long-lived connection has no boundary like this; the server can write whenever it wants, the
client can read whenever it wants, and precisely because of this, what this lesson measures
is not "what a request carries" but "where in the stream a message ends."

This difference also determines the choice of measurement tool. The HTTP lessons built a
real client and server object and measured a single exchange between them; this lesson uses
a single shared byte buffer as the common ground for both the writing and the reading side,
because what is meant to be measured is not the behavior of two separate sides, but the
structure of the **stream itself** — how many messages arrive one after another, and how
these messages are distinguished within the stream.

#### The Connection Is Modeled: Same Interface, Different Concrete Implementation

**ND33.** `bytes.Buffer` implements both `io.Reader` and `io.Writer` — exactly the two
interfaces Go's standard library defines for a network connection. The System and Data
Access topic's first lesson will measure the fact that these two interfaces are shared
between a file, a buffer, and a string; this lesson sets up the connection side of that same
fact in advance, without ever opening the network. The procedures below write a message
first as a four-byte big-endian length prefix, then as the message's own bytes; the reading
side first reads these four bytes to learn the length, then asks for exactly that many
bytes.

This model has a limit, and it needs to be stated explicitly: on a real network connection,
when a `Write` call reaches the other side, and into how many pieces a single call gets
split as it proceeds, can depend on factors like latency and ordering. This lesson models
none of that — a write over `bytes.Buffer` completes instantly, and the next read always
sees the most current state. What is measured is not latency or ordering, only **framing
itself**: how the messages embedded inside a stream get parsed by the side reading the
stream. This narrow scope is deliberate; an extended model is built on top of the same
framing rule, it does not change it.

```go
// framing.go — measuring how framing carries the message boundary in a long-lived connection
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
)

func writeFrame(conn *bytes.Buffer, message string) {
	var length [4]byte
	binary.BigEndian.PutUint32(length[:], uint32(len(message)))
	conn.Write(length[:])
	conn.WriteString(message)
}

func readFrame(conn *bytes.Buffer) (string, bool) {
	var length [4]byte
	if _, err := io.ReadFull(conn, length[:]); err != nil {
		return "", false
	}
	size := binary.BigEndian.Uint32(length[:])
	body := make([]byte, size)
	if _, err := io.ReadFull(conn, body); err != nil {
		return "", false
	}
	return string(body), true
}

func main() {
	pushed := []string{"welcome", "data", "closing"}

	var unframedConn bytes.Buffer
	for _, m := range pushed {
		unframedConn.WriteString(m)
	}
	fmt.Println("-- in an unframed connection, three pushes merge into a single byte sequence --")
	correctlyParsed := 0
	for {
		chunk := make([]byte, 6)
		n, err := unframedConn.Read(chunk)
		if n == 0 {
			break
		}
		chunkText := string(chunk[:n])
		fmt.Printf("read chunk: %q\n", chunkText)
		for _, m := range pushed {
			if chunkText == m {
				correctlyParsed++
			}
		}
		if err != nil {
			break
		}
	}
	fmt.Printf("correctly parsed message: %d/%d\n", correctlyParsed, len(pushed))

	var framedConn bytes.Buffer
	for _, m := range pushed {
		writeFrame(&framedConn, m)
	}
	fmt.Println()
	fmt.Println("-- in a framed connection, all three pushes parse back with the boundary the server wrote --")
	correctlyParsedFramed := 0
	for i := 0; i < len(pushed); i++ {
		m, ok := readFrame(&framedConn)
		if !ok {
			break
		}
		fmt.Printf("read message: %q\n", m)
		if m == pushed[i] {
			correctlyParsedFramed++
		}
	}
	fmt.Printf("correctly parsed message: %d/%d\n", correctlyParsedFramed, len(pushed))

	var numericPush bytes.Buffer
	writeFrame(&numericPush, "42")
	body, _ := readFrame(&numericPush)
	fmt.Println()
	fmt.Println("-- limiting measure: the frame gives the correct body, but not the body's type --")
	fmt.Printf("body: %q, type: %T\n", body, body)
}
```

```
-- in an unframed connection, three pushes merge into a single byte sequence --
read chunk: "welcom"
read chunk: "edatac"
read chunk: "losing"
correctly parsed message: 0/3

-- in a framed connection, all three pushes parse back with the boundary the server wrote --
read message: "welcome"
read message: "data"
read message: "closing"
correctly parsed message: 3/3

-- limiting measure: the frame gives the correct body, but not the body's type --
body: "42", type: string
```

**ND34.** The three messages in the `pushed` slice are written one after another to
`unframedConn` and `framedConn` — with no read, no "request" between them. This differs from
the request–response pattern established in the HTTP server and client lessons: there, every
response followed a request; here, the server side **pushes** all three times on its own
initiative. This is exactly a long-lived connection's push property — being able to write
without a trigger arriving from the other side.

How a server handles a request was already built as a concept across eight courses of the
Backend Development curriculum, and is not repeated here; this lesson's share is only
whether the two interfaces in Go's standard library (`io.Reader`, `io.Writer`) can carry a
push like this. The answer here is yes: the `bytes.Buffer.Write` call depends on no request
at all, it only appends to the end of the buffer the moment it is called — this shows how
simple, at the standard library level, it is for a server to send data to a client without
notice beforehand. The complexity is elsewhere: the other side needs to know where these
pushes end, and the thing that carries this information is not `Write` itself, it is the
framing measured shortly.

#### Unframed Stream: Three Pushes Merge into a Single Byte Sequence

**ND35.** The three messages written to `unframedConn` (`"welcome"`, `"data"`, `"closing"`)
are appended directly one after another with no separator between them — the result is a
single eighteen-byte sequence. The reading side **has no way of knowing** where one message
ends and the next begins in this sequence; all it knows is to read fixed-size (six-byte)
chunks. None of these chunks matches any of the three original messages — `"welcom"`,
`"edatac"`, `"losing"` — because the six-byte boundary is a completely arbitrary split with
no relationship at all to the message boundaries. The measurement gives this as `0/3`: a
fixed-size read carries **no** guarantee at all of preserving message boundaries.

The choice of six bytes is not a detail of the measurement, it was chosen precisely to show
this arbitrariness: none of the three messages' lengths (7, 4, 7) divide evenly by six, so
no read stops exactly at a message's boundary. If the read size were four, or eight, the
result would still be `0/3`, because the problem is not the read size's magnitude, it is
that the read size is a number chosen from outside, **independent** of the message lengths.
This points to a fundamental property of a long-lived connection: a `Read` call on an
`io.Reader` returns as many bytes as were requested (or however many are available at that
moment) — it carries no information at all about which bytes are the start of a message and
which are the middle. If this information is not in the stream itself, the reading side has
to **invent** it; a fixed-size read is exactly this kind of invention, and this measurement
shows why it does not work.

#### Framed Stream: Parses Back Correctly with Boundary Information

**ND36.** Before every message written to `framedConn` stands a four-byte prefix carrying
that message's length. The reading side no longer guesses: it first reads four bytes to
learn the length, then does a read of exactly that length — neither short nor long. All
three messages come back correctly, byte for byte, `3/3`. The difference is not the read
size's magnitude (in the unframed trial six bytes was longer than some messages and shorter
than others); the difference is that the read size **comes from the message itself every
time**, it is not imposed as a fixed number from outside.

`readFrame` uses `io.ReadFull` in both of its reads, not a plain `Read` call. This choice is
not arbitrary: the `io.Reader` interface's contract permits returning **less** than the
requested byte count, even if more data is available — `bytes.Buffer` always gave exactly
what was requested in this measurement, but that is not part of the interface's general
contract, it is the behavior of this particular concrete type. `io.ReadFull` repeats the
read until the requested byte count is exactly met (either the stream ends or an error
occurs); this is a guarantee against the possibility of the four-byte length prefix or the
variable-length body arriving piecemeal, incomplete, in a single `Read` call. Just as much
as the framing rule itself, the code reading it paying attention to this detail is also part
of parsing correctly.

A length prefix is not the only possible form of framing — a special byte marking the end
of a message (a delimiter) could do the same job. But delimiter-based framing carries the
risk of the same byte as the delimiter itself appearing inside a message's content: the
`"data"` message ends with the letter `"i"`; had `"i"` been chosen as the delimiter, this
message would have looked like it was cut short within itself. A length prefix carries no
such risk, because it never looks at the message's content at all — it first says "how many
bytes are coming," then reads that many bytes, whatever is inside them, as a single unit.
This lesson does not compare these two strategies, it only measures the second one, but this
is the reasoning behind the choice.

The course's full distribution of thirteen boundary crossings (1 preserved, 5 rebuilt, 7
lost; the number built in Lesson 03) solves a separate sub-problem that lies outside these
thirteen crossings: that distribution counted what happens to a single value's type as it
crosses a boundary, while this lesson determines **which part** of a stream is a value in
the first place. Without the answer to this question, neither the previous lessons' JSON
decoder nor the schema generator could work at all — both need, from the start, the
information "a message ended here," and the only thing that carries this information is
framing.

The Computer Networks curriculum's Application Layer Protocols course and the Frontend
Development curriculum's Application Architecture: Routing, State, and Data course already
built long-lived connections and real-time data streaming as concepts; that is not repeated
here, what is measured is only how Go's standard library carries a byte stream at the
interface level.

**ND37.** The final measurement is a warning: the body written with `writeFrame` and read
back with `readFrame` is the string `"42"` — framing works flawlessly here too, the body is
correct byte for byte. But `body`'s Go type is `string`, not `int`. Framing says **where** a
message ends, it does not say whether the bytes inside it represent an integer, a string, or
a struct. Answering this question requires a schema of the kind the previous lesson
measured — framing and schema are two separate layers that complete each other in a
long-lived connection: one draws the boundary, the other names what is inside it.

The importance of this distinction shows up in the fact that a body like `"42"` can be read
**both ways**. If the other side tries to convert this body to an integer (with
`strconv.Atoi`), it succeeds and gets the number `42`; if it keeps it as a string without
converting at all, it also gets a valid value. Framing leaves both interpretations **equally
possible**, because the body's bytes alone do not say which interpretation is correct. The
contract-based schema seen in the previous lesson was exactly what closed this ambiguity: by
fixing a field name and its type in advance, it bound both sides to the same decision about
how the body would be read. In a long-lived connection these two layers are usually set up
together — framing carries the boundary of every message, the schema carries the meaning of
the fields inside every message — but this lesson deliberately separated them, because the
two can succeed or fail independently of each other: a stream can be framed correctly and
still be interpreted incorrectly.

#### Summary

- A long-lived connection was modeled with a `bytes.Buffer` implementing `io.Reader` and
  `io.Writer`, without opening a real network connection; because the measured code uses
  only these two interfaces, the same logic would run unchanged over a real connection too.
- The server side pushed three messages without waiting for a request between them — a
  long-lived connection's push property differs from the previous lessons' request–response
  pattern in exactly this way.
- In an unframed stream, three messages merged into a single byte sequence and fixed-size
  reads could not correctly separate any of them (`0/3`); in a stream framed with a length
  prefix, all three came back correct byte for byte (`3/3`), because the read size came
  from the message itself every time.
- Using `io.ReadFull` was a guarantee for cases where the `io.Reader` contract permits
  returning less than the requested byte count — the framing rule working correctly also
  depended on the code reading it observing this detail.
- The limiting measure: framing gave the correct body but did not give the body's type —
  the string `"42"` parsed correctly, but whether this is an integer or a string is a
  separate schema question that lies outside framing.
- Framing and schema are two separate layers completing each other in a long-lived
  connection: one carries the message's boundary, the other carries the meaning of what is
  inside that boundary.

#### Next Step

This lesson measured the single piece of information that determines which part of a
stream is a message — framing — and showed that this information carries no **type** of a
message at all: what is inside the same frame still requires a schema. This course's topic
(Networking and Data Formats) measured, across its six lessons, values crossing the boundary
always over the network — a request, a response, a template's output, a schema shared by two
processes, a long-lived connection. The next topic asks the same question at the program's
own boundaries: a file, a buffer, and an embedded resource, too, just like this lesson's
connection, speak at the byte level — and there too, on which side of the boundary type
information stands, and on which side it does not, will be measured again.

## System and Data Access

The reader and writer abstractions working at the byte level, a resource binding to compile time by embedding into a binary file, duration and time-zone operations losing information as they turn into text, a key-value log carrying field names, a driver interface's narrow type set and a connection pool's life cycle, a flag parser rebuilding a type from a string.

### File and I/O Interfaces

Source: https://academia.sh/en/courses/go-applications/file-and-io-interfaces

Four separate sources (a file, a buffer, a string and the network body measured earlier) are passed through the same two methods — read and write — and none of them is shown to carry type information across the boundary. The limiting measurement runs a small buffer to show that reading returns the data and the end of file in separate steps, and that managing this distinction is the caller's job.

The last lesson of the Networking and Data Formats topic closed with a question: a framing rule
tells you where a message starts and where it ends, but it does not tell you what the bytes
inside that boundary are. This topic asks the same question without carrying it outside the
program, to the network. A byte read from a file, a byte accumulated in a buffer, and a byte
that just arrived from the network — all three are looking for an answer to the same question:
does a byte sequence coming from the other side say, on its own, what type it belongs to?

The answer is no, and this topic's first lesson shows where that answer comes from: Go's standard
library makes the file, the buffer, the network body and the string speak through a single
shared contract — read and write. This contract is so narrow that it says nothing about the
source itself; it only streams bytes. The Networking and Data Formats topic already measured
that the HTTP body follows this same contract; that is not rebuilt here, and this lesson passes
three **new** sources — a file, a buffer, a string — through the same contract and shows the
same result (that type information does not carry across) for each one separately.

**Determinism note:** the file source sits in a temporary directory built with `os.MkdirTemp`;
the directory's own path is never printed, and only numbers related to its content are used.

#### Read and Write Are Two Methods

Go's standard library has a single condition for a source to count as "readable": carrying a
method with the signature `Read(p []byte) (n int, err error)`. Interfaces themselves — the
implicit implementation model, where a type satisfies an interface only by carrying the right
methods without a separate declaration saying "I satisfy this interface" — were established in
the Interfaces, Errors and Generics course and are not rebuilt here. This lesson measures that
model's most common example in the standard library: the `io.Reader` interface asks for a single
`Read` method, and the file, network, compression, encryption and text-processing packages are
**all** built on top of this one interface. The write side has a symmetric interface:
`io.Writer`, whose only method is `Write(p []byte) (n int, err error)`.

The smallness of these two interfaces is a design decision, not a shortcoming. Because the
only condition for "readable" is this one return signature, types that share no common ancestry
at all — `os.File`, `bytes.Buffer`, `strings.Reader`, a network body — can each satisfy the same
interface **independently**. None of them has to imitate the others; each writes its own `Read`
method against its own internal data, and outside code can run the same loop without ever
knowing which type it holds. The implicit implementation is especially visible here: the word
`io.Reader` never appears in the definition of the `os.File` type, but because it carries a
`Read` method with the right signature, it satisfies the interface — the connection is built by
signature match, not by declaration.

#### Four Sources, One Interface

**SD1.** — The same four-character text (`"bolt"`) is deliberately placed into the three sources
below (a file, a buffer, a string); the goal is to compare the tool's **behavior**, not the
sources' content. The fourth source — the network body — was already run with the same result
(lost) in the Networking and Data Formats topic and is not rebuilt here.

```go
// read.go — a helper that passes three separate sources through the same reader interface
package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
)

func read(r io.Reader) (int, string) {
	b, _ := io.ReadAll(r)
	return len(b), fmt.Sprintf("%T", b)
}

type source struct {
	name string
	r    io.Reader
}

func main() {
	dir, _ := os.MkdirTemp("", "sd1")
	defer os.RemoveAll(dir)
	os.WriteFile(dir+"/content.txt", []byte("bolt"), 0o644)
	file, _ := os.Open(dir + "/content.txt")
	defer file.Close()

	var buffer bytes.Buffer
	buffer.WriteString("bolt")

	str := strings.NewReader("bolt")

	sources := []source{
		{"file", file},
		{"buffer", &buffer},
		{"string", str},
	}
	for _, k := range sources {
		length, kind := read(k.r)
		fmt.Printf("%-10s length=%d type=%s\n", k.name, length, kind)
	}
}
```

```
file       length=4 type=[]uint8
buffer     length=4 type=[]uint8
string     length=4 type=[]uint8
```

The three sources have completely separate implementations: `*os.File` talks to the operating
system through system calls, `*bytes.Buffer` manages a slice that grows in memory, and
`*strings.Reader` walks over an immutable string. The `read` function never sees this difference
— its parameter is `io.Reader`, which expects only the `Read` method. All three produce exactly
the same result: four bytes, and a type that is always `[]uint8` (in Go, `byte` is an alias for
`uint8`; `%T` shows it under its own name). The source's original type — file, buffer, or string
— does not survive anywhere past this point; the only thing the read operation produces is a raw
byte slice.

This is the first concrete example of this topic's measurement axis: the value crossing the
boundary (the data `Read` returns) falls into the **lost** class, because no matter who sent it
back on the other side, the result stays in the same raw type. Four bytes read from a file and
four bytes read from a string are indistinguishable after the read operation.

The power of implicit implementation does not stop at the standard library's own types. Any type
that writes the `Read` signature correctly satisfies the `io.Reader` interface — even a
handwritten type that never touches the filesystem or memory.

```go
// handwritten.go — a type that writes its own Read method also satisfies the io.Reader interface
package main

import (
	"fmt"
	"io"
)

// RepeatReader is a handwritten reader that repeats one byte the requested number of times.
type RepeatReader struct {
	Byte      byte
	Remaining int
}

func (t *RepeatReader) Read(p []byte) (int, error) {
	if t.Remaining == 0 {
		return 0, io.EOF
	}
	n := 0
	for n < len(p) && t.Remaining > 0 {
		p[n] = t.Byte
		t.Remaining--
		n++
	}
	return n, nil
}

func main() {
	var source io.Reader = &RepeatReader{Byte: 'x', Remaining: 5}
	data, _ := io.ReadAll(source)
	fmt.Printf("data from our handwritten reader: %q type=%T\n", data, data)
}
```

```
data from our handwritten reader: "xxxxx" type=[]uint8
```

`RepeatReader` neither opens a file nor wraps a string; it produces bytes according to its own
`Remaining` counter. Yet `io.ReadAll` sees no difference between it and the other three sources,
because the only thing it looks for is a `Read` method with the right signature. This shows that
the four sources' common ground is actually open to a fifth, a sixth, any number of new sources
— the interface stays fixed; the number of implementers is unbounded.

#### The Write Side: io.Writer

The write direction has the same symmetry. When data is written to a destination that
implements `io.Writer`, the writer does not care whether the destination is a file or a buffer;
the only thing that matters is how many bytes were written.

**SD2.** — The same five-byte data (`"screw"`) is written to both a file and a buffer; both are
then read back and checked for equality with each other and with the source.

```go
// write.go — the same data is written to two different writer destinations
package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
)

func write(w io.Writer, data []byte) (int, string) {
	n, _ := w.Write(data)
	return n, fmt.Sprintf("%T", w)
}

func main() {
	dir, _ := os.MkdirTemp("", "sd2")
	defer os.RemoveAll(dir)
	file, _ := os.Create(dir + "/output.txt")

	var buffer bytes.Buffer

	n1, t1 := write(file, []byte("screw"))
	file.Close()
	n2, t2 := write(&buffer, []byte("screw"))

	fmt.Printf("written to file    n=%d type=%s\n", n1, t1)
	fmt.Printf("written to buffer  n=%d type=%s\n", n2, t2)

	readBack, _ := os.ReadFile(dir + "/output.txt")
	fmt.Println("read back from file   :", string(readBack))
	fmt.Println("read back from buffer :", buffer.String())
	fmt.Println("are the two equal     :", string(readBack) == buffer.String())
}
```

```
written to file    n=5 type=*os.File
written to buffer  n=5 type=*bytes.Buffer
read back from file   : screw
read back from buffer : screw
are the two equal     : true
```

The `write` function's signature is symmetric to `read`'s: its parameter is not a concrete
destination but the `io.Writer` interface. `*os.File` and `*bytes.Buffer` are two types that do
not resemble each other at all, yet both can pass through the same function because both carry a
`Write` method. The result is symmetric too: the same five bytes are written to both, and the
content read back from each is equal to the other. This line adds one more answer to the
topic's question — on the write side too, the value crossing the boundary is carried as the same
raw byte sequence regardless of the destination's own type; whether the destination is
`*os.File` or `*bytes.Buffer` never affects the content written.

#### Composition: io.Copy and bufio Are Built on the Same Interface

Narrow interfaces let the standard library write generic tools between a source and a
destination. `io.Copy(dst io.Writer, src io.Reader)` moves data from a source to a destination
without ever knowing the concrete type of its parameters — the source can be a file, a buffer,
a string, or any other `io.Reader`; the destination can likewise be any `io.Writer`. The same
idea holds in `bufio`: `bufio.NewScanner` takes an `io.Reader` and turns it into a wrapper that
reads it line by line (or word by word), without ever asking whether the source it wraps is a
file or a string.

```go
// combine.go — io.Copy and bufio work on any reader/writer without knowing the concrete type
package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	dir, _ := os.MkdirTemp("", "sd4")
	defer os.RemoveAll(dir)
	os.WriteFile(dir+"/source.txt", []byte("nut and bolt"), 0o644)
	file, _ := os.Open(dir + "/source.txt")
	defer file.Close()

	var dest bytes.Buffer
	n, _ := io.Copy(&dest, file)
	fmt.Println("bytes moved from file to buffer :", n)
	fmt.Println("buffer contents                 :", dest.String())

	lineScanner := bufio.NewScanner(strings.NewReader("first line\nsecond line"))
	lineCount := 0
	for lineScanner.Scan() {
		lineCount++
	}
	fmt.Println("line count counted by bufio:", lineCount)
}
```

```
bytes moved from file to buffer : 12
buffer contents                 : nut and bolt
line count counted by bufio: 2
```

The `io.Copy` call's signature contains no concrete type at all — no file name, no buffer kind,
just two interfaces. This function is written once in the standard library and is reusable
in every combination — file to network, network to buffer, buffer to another file; no matter how
many concrete types the source and destination have, `io.Copy` itself never changes.
`bufio.NewScanner` follows the same principle: its input is an `io.Reader`, and although it
wraps another `io.Reader`, it offers a richer interface (line-by-line scanning) — small
interfaces composing on top of each other is the direct result of producing small, pluggable
pieces instead of writing one large "file reader" type.

#### Limiting Measurement: Partial Reads and End of File

The two measurements so far used `io.ReadAll` — a helper that runs the loop internally until the
source is exhausted. But the `Read` method itself does not guarantee this: a single call is not
required to return the full number of requested bytes, and the data ending and the error
arriving are not required to happen in the same call. This distinction is written clearly in the
standard library's documentation, but it is hard to see without running it.

**SD3.** — A five-byte string is deliberately read with a two-byte buffer; how many bytes arrive
at each step, and at which step the error appears, are printed separately.

```go
// partial.go — reading with a small buffer returns the data and end of file in separate steps
package main

import (
	"fmt"
	"strings"
)

func main() {
	source := strings.NewReader("board")
	buffer := make([]byte, 2)
	total := 0
	step := 0
	for {
		n, err := source.Read(buffer)
		step++
		fmt.Printf("step=%d read=%d chunk=%q err=%v\n", step, n, buffer[:n], err)
		total += n
		if err != nil {
			break
		}
	}
	fmt.Println("total bytes read:", total)
}
```

```
step=1 read=2 chunk="bo" err=<nil>
step=2 read=2 chunk="ar" err=<nil>
step=3 read=1 chunk="d" err=<nil>
step=4 read=0 chunk="" err=EOF
total bytes read: 5
```

The five-byte string "board" requires four separate `Read` calls when read with a two-byte
buffer — the third step leaves a single byte, and the error is still `nil` at that step. The
end-of-file signal (`io.EOF`) arrives only on the fourth call, with no data at all. This is a
contract of the source's behavior: `Read` can return a byte count greater than `0` and a
non-nil error at the same time in a single call, or it can split the data and the error across
separate calls. Code that relies only on a helper like `io.ReadAll` never sees this distinction,
because the helper completes the loop internally. But code that calls `Read` directly can skip
the last byte at the third step if it assumes the data ending and the error arriving happen in
the same step.

This is the boundary specific to this lesson on the topic's measurement axis: the read
interface does not carry a loss — all five bytes arrived in the right order — but it never
takes on the job of separating a partial read from the end of file. That distinction stays the
responsibility of the code calling `Read`; the standard library's `io.ReadFull` and
`io.ReadAll` helpers take on this responsibility, but they are tools the source itself does not
require.

For an in-memory source like `strings.NewReader`, managing this distinction by hand looks
unnecessary — all the data is already ready in memory, and `Read` not giving everything at once
is just a matter of choice. But the interface's contract is the same for every implementer; for
an `io.Reader` that reads from a network socket or a pipe, a partial read is not a choice, it
comes from the nature of the source — if the other side sends the data in pieces, `Read`
returning everything in a single call is not possible. The `io.Reader` contract does not
distinguish between these two cases (an in-memory source and a source arriving in pieces); both
carry the same signature, and code that correctly manages partial reads works for both.

#### Summary

- `io.Reader` and `io.Writer` are two of the language's narrowest interfaces; they ask for a
  single `Read` and a single `Write` method respectively, which is why types that resemble each
  other not at all — a file, a buffer, a string — can each satisfy the same contract
  independently.
- The same content read from a file, a buffer and a string becomes indistinguishable after the
  read operation: all three give a result of the same length and the same `[]uint8` type — the
  value crossing the boundary carries no type information.
- The same symmetry holds in the write direction too: `*os.File` and `*bytes.Buffer` pass
  through the same `write` function, and the content written to each turns out equal to the
  other, byte for byte.
- Tools like `io.Copy` and `bufio.NewScanner` work without ever knowing the concrete type; small
  interfaces can therefore compose freely across a file, a buffer and a string.
- A partial read and the end of file are separate events: reading with a small buffer can split
  the data across several steps, and the end-of-file signal arrives independently of the data,
  in its own step.
- Managing this distinction is the caller's job; helpers like `io.ReadAll` take on this
  responsibility, but the interface itself does not require it.

#### Next Step

All three sources measured in this lesson sat somewhere reachable while the program was
running: on a filesystem path, or in the program's own memory. But the data a program needs is
not always available while it is running — a configuration file, an HTML template, a static
asset, can instead travel inside the program's own binary. The next lesson looks at the
mechanism that moves this link to compile time: how does a source file sit directly inside a
compiled program without ever being looked up while it runs?

### Embedded Resources

Source: https://academia.sh/en/courses/go-applications/embedded-resources

A source file is measured as it binds to three separate Go types (a string, a byte slice, a filesystem) with the same content through the go:embed directive, and that content is shown to match the file sitting on disk exactly. The limiting measurement runs a compiled binary to show that it produces the same content even after the source file is deleted from disk: the link is built at compile time, and the file is never needed again at run time.

The previous lesson passed three sources (a file, a buffer, a string) through the same reader
interface and showed that all of them give the same result — a raw byte slice. Those three
sources shared one property: they stayed reachable while the program was still running — the
file sat on disk, the buffer sat in memory. This lesson asks a different question: can data a
program needs be in the program's hands even when it is never reachable while running? The
answer is yes, and the method is not a read but a **binding**: placing the source file's content
directly inside the binary at compile time.

**Term note:** the word "embedding" used in this lesson shares its name with the struct
embedding measured in the Composite Types topic (placing one type unnamed among another type's
fields), but it is a separate mechanism. Struct embedding was a relationship built between two
Go types inside the source code; the **embedded resource** measured in this lesson instead
carries a file's content, from outside the source code, into a variable through the compiler
itself. The two share only a name — one binds two Go types to each other, the other binds a
file to a Go value.

**Determinism note:** no file path is printed in this lesson; the only things measured are the
content's length, its type, and whether the two representations are equal to each other.

This mechanism's typical use cases are data that does not change while running but still needs
to travel with the program: a configuration file's default values, a command-line tool's help
text, a web server's static assets. What they all share is that the content is **part of the
program** — not data copied alongside it through a separate setup step.

#### The Source File

The embed directive (`//go:embed`) is a special comment written directly above a `var`
declaration; the compiler reads it and places the specified file's content into that variable at
compile time. For the directive to work, the file to embed has to exist first — the shell block
below creates the `recipe.txt` file that the rest of the lesson embeds, and reads it back to
verify its content.

```bash
# recipe-file.sh — creates the source file that go:embed will embed
cat > recipe.txt <<'EOF'
the source travels into the package with code
EOF
cat recipe.txt
```

```
the source travels into the package with code
```

#### One File, Three Types

**SD4.** — The same `recipe.txt` file is embedded into three separate variables: a string, a
byte slice, and a filesystem type (`embed.FS`). The goal is to show that the directive behaves
differently depending on the target type, and that all three carry the same content.

```go
// embedded.go — the same file is embedded as string, []byte and embed.FS, and their content is compared
package main

import (
	"embed"
	"fmt"
	"os"
)

//go:embed recipe.txt
var recipeText string

//go:embed recipe.txt
var recipeBytes []byte

//go:embed recipe.txt
var recipeFS embed.FS

func main() {
	fromFS, _ := recipeFS.ReadFile("recipe.txt")
	fromDisk, fileErr := os.ReadFile("recipe.txt")

	fmt.Printf("recipeText type=%T length=%d\n", recipeText, len(recipeText))
	fmt.Printf("recipeBytes type=%T length=%d\n", recipeBytes, len(recipeBytes))
	fmt.Println("text == byte conversion:", recipeText == string(recipeBytes))
	fmt.Println("text == embed.FS read:", recipeText == string(fromFS))
	fmt.Println("text == read from filesystem:", recipeText == string(fromDisk), fileErr == nil)
}
```

```
recipeText type=string length=46
recipeBytes type=[]uint8 length=46
text == byte conversion: true
text == embed.FS read: true
text == read from filesystem: true true
```

The three `//go:embed` directives are independent of each other; each reads the same
`recipe.txt`, but produces a different representation depending on the target variable's type.
`recipeText` is a string, `recipeBytes` is a byte slice, and `recipeFS` is a small filesystem
abstraction with methods like `Open` and `ReadFile`. Because all three come from the same
source, their content matches exactly — the same 46 bytes come back whether compared through the
byte conversion, read through `embed.FS`, or read live from the file itself with `os.ReadFile`.
This last comparison builds a bridge to the previous lesson: while the file is still on disk, the
embedded content and the content on disk are indistinguishable — as if the file were read twice,
but one read happens at compile time and the other at run time.

Splitting the same file into three separate representations is not arbitrary; each
representation fits a different way of using it. Code that uses the text directly as a string (a
fixed message, a template source) prefers `recipeText`. Code that hands the content off to
another binary format (a compression algorithm, a network body) prefers `recipeBytes`, because
`[]byte` is the type those kinds of interfaces expect. Code that needs to carry more than one
file, or a directory structure, needs `embed.FS` — that need is measured in the next section.

#### Limiting Measurement: The Source File Is Never Looked Up at Run Time

The measurement so far ran while `recipe.txt` was still on disk, so the claim "the embedded
content matches the file" could not be separated from the claim "the program depends on the
file." The step below separates the two: the same source is first compiled into a standalone
binary, then the source file is deleted from disk, and only then is that binary run.

**SD5.** — The build step produces a separate binary with `go build`; `go run` is not used,
because `go run` recompiles on every call and the source file would still be on disk at that
moment. The goal here is to separate compiling from running.

```bash
# build-and-delete.sh — first it compiles, then the source file is deleted, then only the binary runs
go build -o body_program .
rm recipe.txt
./body_program
```

```
recipeText type=string length=46
recipeBytes type=[]uint8 length=46
text == byte conversion: true
text == embed.FS read: true
text == read from filesystem: false false
```

The first four lines of the output are exactly the same as the previous run: `recipeText`,
`recipeBytes` and `recipeFS` still give the correct 46-byte content — even though `recipe.txt` is
no longer on disk. Only the last line changes: `os.ReadFile("recipe.txt")` can no longer find
the file, so both the comparison and the error check turn to `false`. This difference shows the
real distinction between the embed directive and a live file read — the first three variables
absorbed the content into themselves at compile time and never read the source again after that
moment; the fourth line looks the file up again on every run.

On the topic's measurement axis: the link from the source file to the embedded variable is not
built at run time — it is already built at compile time. This is exactly where it differs from
reading a file (the previous lesson's subject): a read goes back to the source on every call,
while embedding goes to the source only **once**, at compile time, carrying the result into the
binary. This is as much a guarantee as a constraint: the distributed binary does not have to
carry a separate data file alongside it, and that file being changed or deleted at run time
never affects the program's behavior.

This comes with a cost, the flip side of the guarantee: when the content is updated, the binary
has to be recompiled too. The file read in the previous lesson could see a changed source
immediately — reading the file again each time was enough. For an embedded resource this path
is closed; the only way to update the content is to change the source file and recompile. Which
approach is right depends on the content's own nature: reading suits data that changes often,
embedding suits data that should never change at all.

#### Multiple Files: Embedding by Pattern

The directive's target does not have to be a single file name — it can also be a **pattern**.
Because `string` and `[]byte` can only carry a single file's content, the only way to embed more
than one file is `embed.FS`; the result is a small directory tree embedded inside the binary.

**SD6.** — A small two-file directory (`resources/`) is embedded into a single `embed.FS`
variable through a pattern (`resources/*.txt`); the measurement reads the number of embedded
files and each one's length.

```bash
# resources-dir.sh — creates the two-file directory that will be embedded by pattern
mkdir resources
printf 'text from the first file\n' > resources/first.txt
printf 'text from the second file that is longer\n' > resources/second.txt
ls resources
```

```
first.txt
second.txt
```

```go
// directory.go — the content of a directory embedded by pattern is listed and read
package main

import (
	"embed"
	"fmt"
	"sort"
)

//go:embed resources/*.txt
var resourceDir embed.FS

func main() {
	entries, _ := resourceDir.ReadDir("resources")
	names := make([]string, 0, len(entries))
	for _, g := range entries {
		names = append(names, g.Name())
	}
	sort.Strings(names)
	fmt.Println("embedded file count:", len(names))
	for _, name := range names {
		content, _ := resourceDir.ReadFile("resources/" + name)
		fmt.Printf("%s length=%d\n", name, len(content))
	}
}
```

```
embedded file count: 2
first.txt length=25
second.txt length=41
```

The `resourceDir.ReadDir("resources")` call behaves as if it were listing a real directory — it
gives file names, whether they are files, their sizes — but what it lists is not on disk, it is
inside the binary. This is possible because `embed.FS` satisfies the standard library's filesystem abstraction
(the `fs.FS` interface in the `io/fs` package); any tool that behaves like a filesystem can work
without knowing whether the source is a real disk or a copy embedded inside the binary. This interface distinction is not rebuilt here with the server-side details
established in the Networking and Data Formats topic; the only thing measured is that the
embedded directory can be listed and read like a real one.

This is another example of the previous lesson's `io.Reader`/`io.Writer` composition: `fs.FS` is
likewise a narrow interface with just a few methods, and a function that expects it can work
with `os.DirFS("a/real/path")`, with `embed.FS`, or with a handwritten fake filesystem built for
tests. The `resourceDir` variable's concrete type is `embed.FS`, but when you hand it to a
function that expects `fs.FS`, that function never knows whether the content sits on a real disk
or inside the binary — just as the `read` function in the previous lesson never knew whether the
source was a file or a buffer.

#### Compile-Time Boundaries

For the embed directive to work, the target file has to exist in the package's own directory at
compile time. Testing these two constraints separately is possible with the `compiles`-style
helper established in Go's Design Goals lesson and used throughout the course: a function that
writes the source into a temporary directory, attempts to compile it, and classifies the result
as compiled/did-not-compile.

**SD7.** — Three sources are tried: an embed whose file genuinely exists, an embed whose file
does not exist at all, and an embed that tries to reach outside the package's own directory
(`../`). The compiler's error text is not printed here either; only the rejection class is read.

```go
// compiles.go — the embed directive's compile-time constraints are tested with compiles
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string, otherFiles map[string]string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	for name, content := range otherFiles {
		os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755)
		os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644)
	}
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func makeSource(fileName string) string {
	return fmt.Sprintf(`package main
import (
	_ "embed"
)
//go:embed %s
var data string
func main() {}
`, fileName)
}

func main() {
	existing := compiles(makeSource("present.txt"), map[string]string{"present.txt": "content"})
	missing := compiles(makeSource("missing.txt"), nil)
	outsidePkg := compiles(makeSource("../outside.txt"), nil)

	fmt.Println("embedding an existing file       :", existing)
	fmt.Println("embedding a missing file         :", missing)
	fmt.Println("embedding a file outside package :", outsidePkg)
}
```

```
embedding an existing file       : true
embedding a missing file         : false
embedding a file outside package : false
```

The first of the three attempts compiles, the other two do not — but the two rejections are not
for the same reason. The missing file fails the directive's most basic condition: there is no
content to embed at all. The file outside the package is rejected even though its content
exists, because the embed directive can only reach files inside its own package's directory and
below; a path climbing to a parent directory is not accepted at compile time even if the file
genuinely sits there. This is another difference between reading and embedding: `os.ReadFile`
can reach any path the calling code can access, while the embed directive can never step outside
the package's own boundaries.

This boundary is not arbitrary: if a package carries an embed directive that reaches outside its
own directory, someone who picks up the package elsewhere (another module, another machine) and
compiles it cannot know where the source sits — the parent directory does not travel with the
package. An embed confined to the package's own directory guarantees the package is
**self-contained**: copying it automatically copies every embedded resource with it.

One more constraint is not run here but is worth mentioning: files whose name starts with a dot
or an underscore (`.hidden`, `_helper`) are not included in the directive's default pattern; a
directive that wants to embed them too needs an `all:` prefix added to the front. This belongs
to the same family: the embed directive carries its own default rules for which files count as
"really part of the source."

#### Summary

- The `//go:embed` directive places a file's content into a Go variable at compile time; it
  produces a different representation depending on the target type (`string`, `[]byte`,
  `embed.FS`). What crosses the boundary is still bytes; the **target type itself rebuilds** the
  type.
- Three variables bound to the same file give content equal to each other and to a live read of
  the file on disk — while the file is still on disk.
- Limiting measurement: a compiled binary still produces the correct embedded content after the
  source file is deleted — the link is built at compile time, the source is never needed again
  at run time, and only a live disk read (`os.ReadFile`) fails.
- A pattern can embed more than one file into a single `embed.FS` variable; the result is a
  structure that satisfies the `fs.FS` interface and can be listed and read like a real
  directory.
- The embed directive is limited by two compile-time constraints: the target file has to
  genuinely exist, and it cannot reach outside the package's own directory; either one stops the
  build.
- "Embedding" here is a separate mechanism from the struct embedding in the Composite Types
  topic: one is a relationship inside the code, the other is a link the compiler builds from a
  file to a variable.

#### Next Step

The link measured in this lesson was about time, but time itself was never measured — only the
order "compiled first, then deleted, then run" mattered. This ordering completes the answer the
topic's first two lessons gave to the question of what a value crossing the boundary carries: the
file, the buffer and the string never carried type information at all; the embedded resource
does carry the content, but it does so by crossing the boundary at compile time, not at run time
— the boundary itself shifts with time.

The next lesson asks about time itself: what a value carries and what it loses when it turns
into text and comes back. What does a moment gain when it binds to a time zone, and how much of
that gain survives being written to text and read back? There is no equivalent here of the
"content is fixed" guarantee the embed directive gave — time, by definition, is bound to the
very moment it is measured, and writing that moment to a string and reading it back produces a
different kind of loss than reading an embedded file does.

### Time Operations

Source: https://academia.sh/en/courses/go-applications/time-operations

A moment turning into text and coming back is measured: the moment itself is preserved through Equal, but even though the numeric time zone offset is rebuilt, the zone's name does not carry across. time.Duration is shown to be a value carried by its own type, not a plain number. The limiting measurement runs two values representing the same moment to show that they come out equal with Equal while == and String() show them as different.

The previous lesson showed that a link can be built at compile time instead of run time: a
source file's content froze together with the program itself. Time offers exactly the opposite
example — no value is as tied to the run itself as a **moment**. A moment is defined by the very
instant it is measured; writing it to text and reading it back leads to a different kind of loss
than the ones measured in the previous two lessons.

This difference has a concrete consequence: in the file and buffer lesson, the value crossing
the boundary never carried type information at all — the loss was complete every time. With a
time value the situation is more subtle: the text crossing the boundary carries some information
and not other information, and which one falls into which category depends on the layout used
for writing it. This lesson puts three separate values (a moment, a time zone, a duration)
through the same "turn into text and come back" test and measures separately how much of each
survives.

**Determinism note:** the real clock is never read in this lesson (`time.Now` is not called
anywhere). Every example rests on a single moment parsed from a fixed text; the comparisons are
also built on top of this fixed moment.

#### Starting from a Fixed Moment

The type representing a moment in Go's standard library is `time.Time`; `time.Parse` reads a
string according to a given **layout** and produces a `time.Time` value. The layout used in this
lesson is RFC 3339 — a time format common in network and file formats that also carries a time
zone.

```go
// fixed-moment.go — the real clock is not read, the moment is parsed from a fixed text
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, err := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")
	if err != nil {
		panic(err)
	}
	fmt.Printf("parsed value type=%T\n", fixedMoment)
	fmt.Println("time zone (Z = UTC)  :", fixedMoment.Location())
}
```

```
parsed value type=time.Time
time zone (Z = UTC)  : UTC
```

The `Z` at the end of the string means "UTC, zero offset" in the RFC 3339 layout; `time.Parse`
recognizes this and binds the result directly to the `time.UTC` location. The only "real" data
used throughout this lesson is this one fixed string; every step after it is derived from this
value. `time.Parse`'s second argument is the text to parse, and its first argument is the
expected **layout** — not the text itself, but an example of the text's format. This example is
not just a placeholder, it is a real date: the `time.RFC3339` constant is defined in Go's
standard library as `2006-01-02T15:04:05Z07:00`, and this specific date (the digits corresponding
to month/day/hour/minute/second/year in Go's own internal ordering) is the standard way of
specifying a layout — a different approach from another language's abstract placeholders like
`YYYY-MM-DD`.

A `time.Time` value does not always carry the same kind of data. Only values coming from a
`time.Now()` call carry a **monotonic** reading used for duration measurements (separate from
the wall clock, a count that only moves forward); values derived from text through something
like `time.Parse` never carry this extra reading, they are only a plain wall-clock value. Because
this lesson only uses `time.Parse`, every value measured is of this second, "plain" kind.

#### When Time Turns into Text and Comes Back

**SD8.** — The fixed moment is turned into text with `Format` and parsed back with the same
layout. This is the crossing that also appears as one line in the Networking and Data Formats
topic's shared core (a time value to text and back: rebuilt); that topic's binding 1/5/7
distribution is referenced here in a single sentence, the core is not rebuilt. This lesson
verifies that line and extends it with a detail specific to time zones.

```go
// back-to-text.go — a moment is written to text and read back with the same layout
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, _ := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")

	text := fixedMoment.Format(time.RFC3339)
	back, _ := time.Parse(time.RFC3339, text)

	fmt.Println("written back to text :", text)
	fmt.Println("same moment (Equal)  :", back.Equal(fixedMoment))
	fmt.Printf("returned type        : %T\n", back)
}
```

```
written back to text : 2026-08-23T10:00:00Z
same moment (Equal)  : true
returned type        : time.Time
```

When a `time.Time` value is written to a string, the type information is lost completely — the
result is a plain string, with no marker inside it saying "this is a time." But the code calling
`time.Parse` already knows the string in its hand is a time, and rebuilds it by supplying the
right layout; the information that slipped away is **rebuilt** with this knowledge on the
caller's side. The `Equal` result is `true`: the rebuilt moment is exactly the same as the
original moment. On the topic's measurement axis, this line falls into the "rebuilt" class —
just like a struct tag being rebuilt from JSON, what makes the rebuild possible here is not the
string itself, but the layout the caller already knows.

#### The Time Zone's Name Does Not Carry Across

The measurement above hides one detail: the fixed moment is exactly at UTC, that is, at zero
offset. Because the RFC 3339 layout marks zero offset specially with the letter `Z`, the rebuilt
value also binds exactly to `time.UTC`. The picture changes when an offset other than zero is
involved.

**SD9.** — The fixed moment is converted to a time zone named `ZoneA` (not a real geographic
region, just a **fictional** time zone with a +03:00 offset), written to text and read back. The
measurement compares both the numeric offset and the zone name separately.

```go
// zone-name.go — the numeric offset is preserved, the zone's name is not carried across
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, _ := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")
	zoneA := time.FixedZone("ZoneA", 3*3600)
	local := fixedMoment.In(zoneA)

	text := local.Format(time.RFC3339)
	back, _ := time.Parse(time.RFC3339, text)

	localName, localOffset := local.Zone()
	backName, backOffset := back.Zone()

	fmt.Println("written back to text   :", text)
	fmt.Println("same moment (Equal)    :", local.Equal(back))
	fmt.Println("offset preserved       :", localOffset == backOffset)
	fmt.Println("zone name carried over :", localName == backName)
}
```

```
written back to text   : 2026-08-23T13:00:00+03:00
same moment (Equal)    : true
offset preserved       : true
zone name carried over : false
```

The RFC 3339 layout never carries a time zone's name, it only writes its numeric offset
(`+03:00`). When read back, `time.Parse` rebuilds this offset correctly — the moment is still the
same, `Equal` is still `true`, the offset comparison is also `true`. But the zone name
(`"ZoneA"`) never comes back, because it is never written to the text anywhere; the name of the
new location `Parse` produces comes out different from the original `"ZoneA"`. This reads the
same way as the previous lesson: the numeric offset is **rebuilt**, because the text itself
carries it; the zone name is **lost**, because the text itself never carries it at all. The same
crossing falls into two different classes depending on which part of the information is being
carried.

#### time.Duration Is a Type, Not a Number

A duration itself also carries a separate type in Go: `time.Duration`, a duration in nanoseconds
that stores an `int64` underneath but is a type in its own right. Unlike a plain number, its
difference is that it carries its own formatting (like `1h30m0s`), and mixing it up with another
kind of number is rejected by the compiler.

**SD10.** — A duration string (`"1h30m"`) is parsed and its type and formatted text are
measured; then whether the same value is equal to a raw integer equivalent, and how the
formatting changes when it is doubled, are run.

```go
// duration-type.go — time.Duration carries its own type, separate from a raw number
package main

import (
	"fmt"
	"time"
)

func main() {
	duration, _ := time.ParseDuration("1h30m")
	fmt.Printf("duration type=%T value=%s\n", duration, duration)
	fmt.Println("equal to int64 equivalent:", int64(duration) == 5400_000_000_000)

	doubled := duration * 2
	fmt.Println("doubled:", doubled)
	fmt.Println("in hours:", doubled.Hours())
}
```

```
duration type=time.Duration value=1h30m0s
equal to int64 equivalent: true
doubled: 3h0m0s
in hours: 3
```

The data underlying the `duration` variable is an `int64` — a count of nanoseconds — but `%T`
shows it not as a raw integer but as `time.Duration`, and when `fmt.Println` prints it directly,
the result is `1h30m0s`, not `5400000000000`. This is a distinction the compiler builds between
an integer and a duration: there is no implicit conversion between the `int64` underlying
`duration` and another `int64` variable; the two are separate types. The multiplication
`doubled := duration * 2` preserves the duration type, and its formatting updates automatically
too — `3h0m0s`, which also gives `3` when queried with the `Hours()` method.

A duration, just like a moment, can also turn into text and come back — and this crossing turns
out less lossy than a moment's crossing.

**SD11.** — The `duration` variable is turned into text with `String()`, and the same text is
read back with `ParseDuration`; a syntactically broken text is also tried.

```go
// duration-to-text.go — a duration turns into text and comes back, an invalid text is rejected
package main

import (
	"fmt"
	"time"
)

func main() {
	duration, _ := time.ParseDuration("1h30m")
	text := duration.String()
	back, backErr := time.ParseDuration(text)
	fmt.Println("duration to text           :", text)
	fmt.Println("same value back            :", back == duration)
	fmt.Println("read back without error    :", backErr == nil)

	_, invalidErr := time.ParseDuration("1h30mx")
	fmt.Println("invalid text gave an error :", invalidErr != nil)
}
```

```
duration to text           : 1h30m0s
same value back            : true
read back without error    : true
invalid text gave an error : true
```

Unlike a moment turning into text, writing a `time.Duration` loses no information at all: the
`back == duration` comparison is `true`, because the text `String()` produces (`1h30m0s`) carries
every unit making up the duration (hours, minutes, seconds) exactly. A duration has no "name" or
"context" — the only information it carries is its own length, and this information fits into
the text exactly. This is a complete contrast with the previous measurement, which could not
carry the time zone's name: whether there is a loss depends on how much information the value
itself carries, not on the writing format used. The last line adds a separate observation: a
syntactically broken duration text does not silently fall to `0`; `ParseDuration` rejects it with
an error — the duration goes through a check here too, not at compile time but at least at parse
time.

#### Limiting Measurement: One Moment, Two Representations

The measurements so far examined a moment in a single time zone. The step below represents the
same moment in two different fictional zones and runs three separate comparison tools (`Equal`,
`==`, `String`) side by side.

**SD12.** — Two fictional zones (`ZoneA` +03:00, `ZoneB` −05:00) are applied to the same fixed
moment. All three give a different answer to the "are they the same" question.

```go
// two-representations.go — the same moment in two zones, measured with three comparison tools
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, _ := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")
	zoneA := time.FixedZone("ZoneA", 3*3600)
	zoneB := time.FixedZone("ZoneB", -5*3600)

	viewA := fixedMoment.In(zoneA)
	viewB := fixedMoment.In(zoneB)

	fmt.Println("ZoneA local time   :", viewA.Format("15:04"))
	fmt.Println("ZoneB local time   :", viewB.Format("15:04"))
	fmt.Println("same with Equal    :", viewA.Equal(viewB))
	fmt.Println("same with ==       :", viewA == viewB)
	fmt.Println("same with String() :", viewA.String() == viewB.String())
}
```

```
ZoneA local time   : 13:00
ZoneB local time   : 05:00
same with Equal    : true
same with ==       : false
same with String() : false
```

The three comparison tools answer three different questions. `Equal` compares the moment the two
values represent — since both belong to the same moment, the result is `true`. The `==` operator
instead compares `time.Time`'s internal structure exactly; because this structure also contains
the time zone information, two values bound to two different zones do not come out equal with
`==`. `String()` differs for the same reason: each one writes its own local time and offset, one
"13:00 +03:00" and the other "05:00 -05:00" — they do not overlap at all at the text level.

The difference among these three is a trap Go's documentation explicitly warns about: the `==`
operator is a comparison that should not be used for `time.Time` values, because code using it
does not answer the question "is it the same moment," but "is it the same representation" — most
of the time the first question is what is wanted, but the operator itself answers the second. On
the topic's measurement axis, this draws a boundary specific to this lesson: even when two values
represent the same moment, the comparison tool itself determines which question is being asked;
the word "same" alone does not say which comparison is meant.

The same distinction holds for ordering too. The `Before` and `After` methods, just like `Equal`,
look at the moment, not the representation; they correctly say which one comes earlier in time
even when the two values are written in different zones.

```go
// ordering.go — Before and After also look at the moment, not the representation
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, _ := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")
	oneHourLater := fixedMoment.Add(time.Hour)

	zoneA := time.FixedZone("ZoneA", 3*3600)
	viewA := fixedMoment.In(zoneA)
	laterViewB := oneHourLater.In(time.FixedZone("ZoneB", -5*3600))

	fmt.Println("is viewA before laterViewB:", viewA.Before(laterViewB))
	fmt.Println("is laterViewB after viewA :", laterViewB.After(viewA))
}
```

```
is viewA before laterViewB: true
is laterViewB after viewA : true
```

Even though the two values are written in completely different zones and completely different
local times, `Before` and `After` find the correct order, because both of them make the
comparison through the represented moment — they do not look for an exact match of the internal
structure the way `==` does. This confirms the previous paragraph's general rule: when comparing
`time.Time` values, `Equal`, `Before` and `After` form a safe foundation, while `==` is safe only
when the two values are known to have been produced in the same zone, in the same way.

#### Summary

- A moment is preserved through `Equal` when it turns into text and is read back with the same
  layout; this is the verified version of the "rebuilt" line in the topic's shared core.
- The RFC 3339 layout carries a time zone's numeric offset but not its name: the rebuilt value's
  offset matches the original while the zone name does not.
- `time.Duration` keeps its own type even though it carries an `int64` underneath; its
  formatting and arithmetic operate according to this type, and it is not mixed up with a raw
  number. Turning into text and coming back also loses no information, because the only thing it
  carries is its own length.
- Limiting measurement: two values representing the same moment come out equal with `Equal` but
  different with `==` and `String()` — the three tools answer three different questions.
- `Before` and `After` also look at the moment, like `Equal`, not the representation; the `==`
  operator is a safe comparison only when the two values are known to have been produced in the
  same zone and format.

#### Next Step

This lesson showed that a time value carries no field names as it turns into text, it only
produces a raw representation — the text `2026-08-23T13:00:00+03:00` carries no marker saying
which field holds which value, it can only be inferred from position. A reader can interpret
this text correctly because it already knows the RFC 3339 layout — just like the embed directive
in the previous lesson placing content into a variable that already knows the target type. But
not every format is this generous: a field inferred from position can no longer be inferred once
the position is lost or the format changes.

The next lesson measures a format that fills exactly this gap: does carrying field names too,
while writing an event to a log, make the same record parseable for both a human and another
program? This lesson's fixed timestamp will be used exactly the same way in the next lesson's
log records — time will again come from a fixed text, not the real clock.

### Structured Logging

Source: https://academia.sh/en/courses/go-applications/structured-logging

The same event is written to a log in two formats: plain text and a key-value based log. The key-value format is measured to carry field names even when the field order changes, preserving the parseability that plain text loses. The limiting measurement shows that even though field names are preserved, types are not: an integer also arrives as text, and rebuilding it requires the reading side's own schema.

The previous lesson showed that a time value carries no field name when it turns into text, only
producing a raw representation that can be inferred from position. This lesson asks the same
boundary question about a log record: is it possible to also carry field names while writing an
event to text, and what does carrying them change? The concept of structured logging itself —
why logs are kept, what a level is, how a logging pipeline is built — was established in the
Observability and Operations course and the Build, Testing and the Ecosystem course, and is not
rebuilt here. This lesson's share is narrow: putting the same event's two representations side by
side and measuring which representation leaves which field parseable.

Where this distinction matters in practice is not the code writing the log, but the side reading
it afterward. A log record is usually not read immediately by a human the moment it is written;
it is accumulated, searched, filtered — when a search tool asks a question like "show records
where count is greater than 10," that tool needs to know which word in the log line is "count."
Plain text leaves this question to the human, while the key-value format gives the tool itself
something to hold onto.

**Determinism note:** the logged event's timestamp is the same fixed text as the previous lesson
(`2026-08-23T10:00:00Z`); the real clock is not read in this lesson either.

#### The Same Event, Two Formats

The `Event` struct below carries a single event to be written to a log: a user name, a number, a
success state, a timestamp. This event is turned into text with two separate functions — one
builds a sentence in natural language, the other lays out field names one after another in
`name=value` format.

**SD13.** — The same `Event` value is printed in two formats, and how many parts each line
splits into when divided by whitespace is counted.

```go
// two-formats.go — the same event is written as plain text and as key-value
package main

import (
	"fmt"
	"strings"
)

type Event struct {
	User    string
	Count   int
	Success bool
	Time    string
}

func plainText(e Event) string {
	return fmt.Sprintf("user %s performed %d unit operations, success=%v (%s)",
		e.User, e.Count, e.Success, e.Time)
}

func keyValue(e Event) string {
	return fmt.Sprintf("time=%s user=%s count=%d success=%t",
		e.Time, e.User, e.Count, e.Success)
}

func main() {
	event := Event{User: "alice", Count: 12, Success: true, Time: "2026-08-23T10:00:00Z"}

	plain := plainText(event)
	kv := keyValue(event)

	fmt.Println("plain text :", plain)
	fmt.Println("key-value  :", kv)

	plainParts := strings.Fields(plain)
	kvParts := strings.Fields(kv)
	fmt.Println("plain text whitespace-separated part count :", len(plainParts))
	fmt.Println("key-value whitespace-separated part count  :", len(kvParts))

	namedFields := 0
	for _, part := range kvParts {
		if _, _, found := strings.Cut(part, "="); found {
			namedFields++
		}
	}
	fmt.Println("named field count inside key-value         :", namedFields)
}
```

```
plain text : user alice performed 12 unit operations, success=true (2026-08-23T10:00:00Z)
key-value  : time=2026-08-23T10:00:00Z user=alice count=12 success=true
plain text whitespace-separated part count : 8
key-value whitespace-separated part count  : 4
named field count inside key-value         : 4
```

The same four fields (user, count, success state, time) spread across eight separate
whitespace-divided parts in the plain-text version — "12" and "unit" are separate parts,
"operations," is one part together with its punctuation mark. Answering the question "what was
the count" retroactively from this line requires knowing the sentence's syntax: which word
position holds the number, where the punctuation falls. The key-value version instead splits the
four fields into four parts, and each part carries its own name; the reading side does not have
to know the sentence's structure, it only looks for the `=` sign.

This difference looks small when reading a single log line by hand — a human understands the
phrase "12 unit" from context. But a logging pipeline's real scale is a stream of thousands of
lines, and the side reading that stream is often not a human, but a tool that filters or counts
by field name. For such a tool, every sentence pattern in plain text is a separate special case:
is the word "performed" always in the third position, or the fourth for some event types? In the
key-value format this question is never asked, because what the tool looks for is a name, not a
position.

#### Order Independence

A real-world property of a logging pipeline: the same event type can be written in different
orders from different code paths — time then user in one place, the exact opposite in another.
This difference is not a deliberate inconsistency; a logging call is usually laid out in the
order the function obtains the values it needs, and this order varies by the calling function.
In plain text this means the sentence changes completely; in the key-value format, as long as
the field names stay fixed, the order remains unimportant.

**SD14.** — The same event is printed with two different field orders; a search function finds
the `count` field by name from both lines.

```go
// order-independence.go — the key-value format is searched independent of field order
package main

import (
	"fmt"
	"strings"
)

type Event struct {
	User    string
	Count   int
	Success bool
	Time    string
}

func keyValueOrdered(e Event, order []string) string {
	parts := make([]string, 0, len(order))
	for _, name := range order {
		switch name {
		case "time":
			parts = append(parts, "time="+e.Time)
		case "user":
			parts = append(parts, "user="+e.User)
		case "count":
			parts = append(parts, fmt.Sprintf("count=%d", e.Count))
		case "success":
			parts = append(parts, fmt.Sprintf("success=%t", e.Success))
		}
	}
	return strings.Join(parts, " ")
}

func findField(log, name string) (string, bool) {
	for _, part := range strings.Fields(log) {
		key, value, found := strings.Cut(part, "=")
		if found && key == name {
			return value, true
		}
	}
	return "", false
}

func main() {
	event := Event{User: "alice", Count: 12, Success: true, Time: "2026-08-23T10:00:00Z"}

	line1 := keyValueOrdered(event, []string{"time", "user", "count", "success"})
	line2 := keyValueOrdered(event, []string{"success", "count", "user", "time"})

	fmt.Println("first code path's log  :", line1)
	fmt.Println("second code path's log :", line2)

	count1, _ := findField(line1, "count")
	count2, _ := findField(line2, "count")
	fmt.Println("count field the same from both orders:", count1 == count2)
}
```

```
first code path's log  : time=2026-08-23T10:00:00Z user=alice count=12 success=true
second code path's log : success=true count=12 user=alice time=2026-08-23T10:00:00Z
count field the same from both orders: true
```

`line1` and `line2` are completely different from each other as text — no field is in the same
position. Yet `findField` finds the same `count` value from both, because what it looks for is a
name, not a position. This search would not be possible in plain text: the only way to tell
whether the word "12" is the count or some other number is to know the sentence's exact
structure, and if two different code paths produce two different sentence structures, the same
parser cannot work for both.

#### When the Value Itself Contains a Space

The measurement above assumed that none of the field values contain a space. `findField` only
splits on whitespace with `strings.Fields`; when a value itself contains a space, this
assumption breaks, and the result is not an error but a **silent truncation**.

**SD15.** — The `user` field is given a two-word value (`"alice smith"`); the same `findField`
function searches for both this field and the unaffected `count` field.

```go
// spaced-value.go — when a value contains a space, the field is silently truncated
package main

import (
	"fmt"
	"strings"
)

func findField(log, name string) (string, bool) {
	for _, part := range strings.Fields(log) {
		key, value, found := strings.Cut(part, "=")
		if found && key == name {
			return value, true
		}
	}
	return "", false
}

func main() {
	logLine := fmt.Sprintf("time=2026-08-23T10:00:00Z user=%s count=%d success=%t",
		"alice smith", 12, true)
	fmt.Println("log line:", logLine)

	user, _ := findField(logLine, "user")
	fmt.Printf("extracted user field  : %q (expected: %q)\n", user, "alice smith")

	count, _ := findField(logLine, "count")
	fmt.Printf("extracted count field : %q\n", count)
}
```

```
log line: time=2026-08-23T10:00:00Z user=alice smith count=12 success=true
extracted user field  : "alice" (expected: "alice smith")
extracted count field : "12"
```

The `user` field is silently truncated to `"alice"` — the word "smith" is left on its own, as a
part with no name, and `findField` does not return an error. The `count` field is not affected
in this example, because the truncation before it changes the number of whitespace-separated
parts, but the following fields' own `=` signs still sit in the right place — but this is by
luck, not a guarantee of the format itself. The key-value format being named and
order-independent never guarantees that values will not carry the separator character (a space,
in this example) inside themselves; production systems that make this format genuinely reliable
use a notation that quotes values and protects them with escape characters — this lesson does
not show that, because the goal is not the format itself, but measuring the basic difference
between unnamed plain text and named key-value.

A notation that removes this fragility entirely was already measured in the Networking and Data
Formats topic: JSON encodes every field with its name and with explicit boundaries (quotes,
commas, curly braces), so a space inside a value never shifts any field. That measurement is not
rebuilt here; what is emphasized here is that a simple `name=value` layout does not carry this
guarantee on its own — being named is one thing, having explicit boundaries is another.

#### Limiting Measurement: Field Names Are Preserved, Types Are Not

The key-value format being order-independent and named shows that field names are preserved. But
only the name is preserved; the field's own Go type (`int`, `bool`) is never written to the log,
all of it turns into text.

**SD16.** — The `count` field is extracted from the log and its type is read with `%T`; then
code that only looks at the field name assumes the likely target type (`int`) with its own
schema and rebuilds the value.

```go
// type-loss.go — the field name is preserved, the type is not; rebuilding relies on the reader's own schema
package main

import (
	"fmt"
	"strconv"
	"strings"
)

func findField(log, name string) (string, bool) {
	for _, part := range strings.Fields(log) {
		key, value, found := strings.Cut(part, "=")
		if found && key == name {
			return value, true
		}
	}
	return "", false
}

func main() {
	logLine := "time=2026-08-23T10:00:00Z user=alice count=12 success=true"

	countText, found := findField(logLine, "count")
	fmt.Printf("count value extracted from the log: %q type=%T found=%t\n", countText, countText, found)

	countNum, parseErr := strconv.Atoi(countText)
	fmt.Printf("rebuilt with the schema (strconv.Atoi): type=%T value=%d noError=%t\n",
		countNum, countNum, parseErr == nil)

	successText, _ := findField(logLine, "success")
	_, wrongTypeErr := strconv.Atoi(successText)
	fmt.Println("reading the success field with the int schema gave an error:", wrongTypeErr != nil)
}
```

```
count value extracted from the log: "12" type=string found=true
rebuilt with the schema (strconv.Atoi): type=int value=12 noError=true
reading the success field with the int schema gave an error: true
```

The first line confirms the expected result on the topic's measurement axis: the value
`findField` returns comes back from the log as a `string`, even though the `count` field is an
`int` on the Go side — `%T` shows it as `string`. Because the field name sits in the log,
`findField` finds the right line, but because the field's type is never written to the log, the
reading side has to guess it again. The `strconv.Atoi` call makes this guess concrete: it
converts the text to an `int`, and this time succeeds, because the calling side already knows
the `count` field is a number — this knowledge does not come from the log, it comes from the
contract of the code that writes the log. The last line shows this guess can be wrong: the
`success` field is the text `"true"`, and trying to read it with an integer schema gives an error
— the wrong schema comes not from the log itself, but from the reading code's assumption. This
error is at least **explicit**: `strconv.Atoi` returns an error value, the program does not crash
or silently produce the wrong number. But this explicitness only shows up when the wrong schema
is actually tried; code that only looks at the log itself finds no marker anywhere that says in
advance which field should be read with which schema.

The answer to where this schema comes from is not in the log itself. The code calling
`findField` does not learn that the `count` field is a number from the log line — it learns this
by looking at the source of the code that writes the log, or from a contract document. As long
as the two sides (writer and reader) share this contract, the system works correctly; when the
contract changes (if `count` is one day converted to a decimal number), the log itself gives no
notice of this at all — `strconv.Atoi` will also reject the new format as "not a valid integer,"
but it will not say why it changed.

#### Summary

- The same event turns into eight whitespace-separated parts as plain text and four as
  key-value; in the key-value format, all four parts arrive together with their name.
- The key-value format is independent of field order: the same event written in two different
  orders gives the same value when searched by the same field name.
- There is no reliable way to find a field by name in plain text; you have to know the
  sentence's exact structure, and that structure can change by code path.
- A simple `name=value` layout can be silently truncated when a value itself contains a space;
  being named is not the same thing as having explicit boundaries.
- Limiting measurement: the key-value format preserves field names but not their types — even an
  `int` field comes back as text, rebuilding it requires the reading side's own schema.
- A wrong schema (trying to read a `bool` field as an `int`) does not pass silently, it is
  rejected with an explicit error — but this rejection only shows up once the wrong schema is
  actually tried.

#### Next Step

In this lesson, field names were preserved but types always turned into text; the reading side
had to infer which field corresponds to which Go type from its own knowledge. This schema always
sat outside the code, in human knowledge or in a contract document — the log itself never said
anything like "this field is an integer."

The next lesson, this topic's last lesson, measures a boundary where the type is reduced this
time not to text, but to a narrow type set the standard library itself recognizes: what types can
a row passing through a database driver arrive in, and what does the target type rebuild from
that row, and how? This time the schema will not sit in text, but in the type of the target
variable the caller hands to the `Scan` call — the role `strconv.Atoi` plays in this lesson will
be taken on there by the standard library itself.

### Database Access

Source: https://academia.sh/en/courses/go-applications/database-access

A five-column query is run with a fake driver: three of the values coming from the driver's narrow type set are rebuilt into a custom Go type with the target variable's own Scan method, two already overlap with the driver's natural type. The limiting measurement runs to show that scanning looks at the target's type and an incompatible target fails at run time, while the connection pool measurement shows that an unclosed row set blocks the pool.

The previous lesson showed that a field extracted from a log line stays as text, and that
rebuilding it depends on the reading side's own schema. This topic's last lesson asks the same
question about a row passing through a database driver — but this time the schema does not sit
in text, but in the code itself: in the type of the target variable the caller hands to the
`Scan` call.

A database row is the most layered boundary measured in this topic. In the file and buffer
lesson the boundary was a single interface (`Read`/`Write`); here there are three layers — the
driver's own narrow type set, `database/sql`'s generic mechanism carrying this set, and the
target type the caller hands to `Scan`. Each layer performs its own share of the conversion, and
this lesson shows separately where all three stand.

**No real source is used.** No real database is run in this lesson; the driver interface in the
standard library's `database/sql/driver` package is implemented by a **fake** this lesson writes
itself. The fake driver returns fixed, hand-prepared two-row data; there is no network
connection, file, or external process.

#### The Fake Driver

Go's standard library offers a generic database interface through the `database/sql` package;
real drivers (connection logic specific to a particular database product) are always distributed
as third-party packages. This lesson takes that distinction seriously: it implements the
interfaces in `database/sql/driver` (`Driver`, `Conn`, `Rows`) with a simple type it writes
itself, and imports no third-party driver at all.

```go
// fake.go — the database/sql/driver interface is implemented with our own fake, no real database
package main

import (
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"time"
)

// UserID carries the row's first column: the driver carries it as an int64.
type UserID int64

func (k *UserID) Scan(source any) error {
	v, ok := source.(int64)
	if !ok {
		return fmt.Errorf("UserID.Scan: unexpected source type %T", source)
	}
	*k = UserID(v)
	return nil
}

// Amount carries an amount in cents; the driver carries this as an int64 too.
type Amount int64

func (t *Amount) Scan(source any) error {
	v, ok := source.(int64)
	if !ok {
		return fmt.Errorf("Amount.Scan: unexpected source type %T", source)
	}
	*t = Amount(v)
	return nil
}

// Timestamp converts a time the driver carries as a string into time.Time.
type Timestamp struct{ time.Time }

func (z *Timestamp) Scan(source any) error {
	s, ok := source.(string)
	if !ok {
		return fmt.Errorf("Timestamp.Scan: unexpected source type %T", source)
	}
	parsed, err := time.Parse(time.RFC3339, s)
	if err != nil {
		return err
	}
	z.Time = parsed
	return nil
}

var rowData = [][]driver.Value{
	{int64(1), "pen", int64(1250), true, "2026-08-23T10:00:00Z"},
	{int64(2), "notebook", int64(3990), false, "2026-08-20T09:30:00Z"},
}

type fakeDriver struct{}

func (fakeDriver) Open(name string) (driver.Conn, error) { return &fakeConn{}, nil }

type fakeConn struct{}

func (fakeConn) Prepare(query string) (driver.Stmt, error) {
	return nil, fmt.Errorf("fake driver: prepared statements not supported")
}
func (fakeConn) Close() error { return nil }
func (fakeConn) Begin() (driver.Tx, error) {
	return nil, fmt.Errorf("fake driver: transactions not supported")
}
func (fakeConn) Query(query string, args []driver.Value) (driver.Rows, error) {
	return &fakeRows{data: rowData, position: 0}, nil
}

type fakeRows struct {
	data     [][]driver.Value
	position int
}

func (fakeRows) Columns() []string {
	return []string{"id", "name", "price_cents", "active", "added"}
}
func (fakeRows) Close() error { return nil }
func (r *fakeRows) Next(dest []driver.Value) error {
	if r.position >= len(r.data) {
		return io.EOF
	}
	copy(dest, r.data[r.position])
	r.position++
	return nil
}

func init() {
	sql.Register("fake", fakeDriver{})
}
```

This file contains no `func main`, so it is written to disk but not run; the measurements below
use it. The `fakeRows.Next` method imitates what a driver actually does: it returns each row in
a `driver.Value` slice, and `driver.Value` is deliberately a narrow type: only `int64`,
`float64`, `bool`, `[]byte`, `string`, `time.Time` and `nil` can enter this type. The result that
matters for this lesson: whatever a column's "real" meaning is at the source (a user ID, a
monetary amount, a timestamp), it is **reduced** to one of these seven types when it crosses the
driver boundary.

This narrowness is deliberate. The `database/sql` package offers a generic contract — every
driver, no matter which product it speaks, has to convert the row's value into one of these
seven types. Calling code can use the same `Query` and `Scan` calls without knowing which driver
it works with, because the difference between drivers stays exactly behind this narrow
interface. This lesson repeats what a real driver does in a minimized form: fixed two-row data,
an `Open` that performs no real operation when attempting to open a connection, and a `Next`
that just returns its own array in order.

#### Five Columns, the Driver's Narrow Type Set

**SD17.** — A five-column query is run; each column is scanned into its own Go type (`UserID`,
`string`, `Amount`, `bool`, `Timestamp`), and the result's type is read with `%T`.

```go
// measurement.go — five columns are scanned into their own types, reduced and non-reduced columns separated
package main

import (
	"database/sql"
	"fmt"
)

func main() {
	db, _ := sql.Open("fake", "in-memory")
	defer db.Close()

	rows, _ := db.Query("select id, name, price_cents, active, added from products")
	defer rows.Close()

	for rows.Next() {
		var id UserID
		var name string
		var price Amount
		var active bool
		var added Timestamp
		if err := rows.Scan(&id, &name, &price, &active, &added); err != nil {
			fmt.Println("scan error:", err)
			continue
		}
		fmt.Printf("id=%d(%T) name=%s(%T) price=%d(%T) active=%t(%T) added=%s(%T)\n",
			id, id, name, name, price, price, active, active, added.Format("2006-01-02"), added)
	}
}
```

```
id=1(main.UserID) name=pen(string) price=1250(main.Amount) active=true(bool) added=2026-08-23(main.Timestamp)
id=2(main.UserID) name=notebook(string) price=3990(main.Amount) active=false(bool) added=2026-08-20(main.Timestamp)
```

Three of the five columns — `id`, `price_cents`, `added` — come from the driver as `int64`,
`int64`, `string` respectively; none of them carries the target type's name (`UserID`, `Amount`,
`Timestamp`). Each of these three columns is **rebuilt** by the target variable's own `Scan`
method — `UserID.Scan` takes the raw `int64` and converts it to its own type, `Timestamp.Scan`
takes the raw string and turns it into a `time.Time` with `time.Parse`. The other two columns —
`name`, `active` — already come from the driver in the same form as the target type (`string`,
`bool`); they need no special `Scan` method, a direct assignment is enough. The result: three of
the five columns are reduced and rebuilt through scanning, two already overlap with the driver's
natural type.

These three `Scan` methods are the topic's measurement axis's clearest example of "rebuilt": the
driver never says anywhere "this is a user ID" or "this is a time," it only gives a raw number or
string. The schema that makes the rebuild possible does not sit in the driver, but in the target
variable's own type and own method, the one the caller hands to `Scan`.

It is not a coincidence that the `name` and `active` columns need no special `Scan` method: the
standard library's own conversion logic makes a direct assignment with no extra step whenever the
target variable's type already overlaps with the `driver.Value` type coming from the driver. A
special `Scan` method only comes into play when the target type falls outside the seven types the
driver recognizes — this is why `UserID`, `Amount` and `Timestamp` carry their own methods, and
`string` and `bool` do not. This shows exactly where the driver's narrow type set's boundary is
drawn: the boundary is fixed at the driver, but every target type that crosses beyond that
boundary has to write its own crossing rule.

#### Limiting Measurement: Scanning Looks at the Target's Type

The `Scan` call above rests on the assumption that the column order matches the target variable
order. When this match breaks — when a column's target does not fit the `driver.Value` type that
column carries — the result is not a compile error, but a **run-time** error.

**SD18.** — The second column (`name`, a `string`) is mistakenly scanned into a `UserID`-typed
target; the query itself never changes, only the variable order in the `Scan` call gets mixed up.

```go
// mismatched-target.go — the query is the same, only the Scan target gets mixed up; the error is at run time
package main

import (
	"database/sql"
	"fmt"
)

func main() {
	db, _ := sql.Open("fake", "in-memory")
	defer db.Close()

	rows, _ := db.Query("select id, name, price_cents, active, added from products")
	defer rows.Close()

	rows.Next()
	var id UserID
	var wrongTarget UserID // the second column (name, a string) is attempted to be scanned in here
	var price Amount
	var active bool
	var added Timestamp
	err := rows.Scan(&id, &wrongTarget, &price, &active, &added)
	fmt.Println("did scanning into a mismatched target give an error:", err != nil)
}
```

```
did scanning into a mismatched target give an error: true
```

The query text (`"select id, name, ..."`) says nothing here — the column names are correct, the
query returns successfully from the fake driver. The error only appears when the `Scan` call
runs: `wrongTarget` is a `UserID`, but the value of the second column coming from the driver is a
`string` (`"pen"`), and `UserID.Scan` returns an error because it cannot convert this to an
`int64`. This is the boundary specific to this lesson on the topic's measurement axis: scanning
looks at the **target's** type, not the **source's** — the column itself does not declare in
advance what type it is, it is only tested at run time against whether it fits the target. A
query does not look like it is written wrong; only the order of the `Scan` arguments has shifted,
and the compiler can never catch this shift because `Scan`'s signature is `...any` — how many
arguments of what type are given is not checked at compile time.

This is a flaw that surfaces in the same way as the log field reading in the previous lesson.
There too, an error with a wrong schema (reading the `success` field as an `int`) came at run
time, not while the query or the log line was being written. The difference here is that the
link between the query's `select` list and `Scan`'s argument list rests on **position**: there is
no check between the two lists that looks at the column name, only the order — and whether this
order is correct is only tested once the program is actually run.

#### Connection Pool and the Responsibility to Close

`sql.DB` is not a single connection, but a **connection pool**; the pool's size can be limited
with `SetMaxOpenConns`. Until a query's result (`*sql.Rows`) is closed, the connection that query
used does not return to the pool — forgetting to close blocks the pool.

**SD19.** — The pool is limited to a single connection. Without closing the first query's result
deliberately, a second query (with a short timeout) is tried on the same pool; then the first
result is closed and a third query is tried.

```go
// pool.go — an unclosed row set blocks a single-connection pool
package main

import (
	"context"
	"database/sql"
	"fmt"
)

func main() {
	db, _ := sql.Open("fake", "in-memory")
	defer db.Close()
	db.SetMaxOpenConns(1)

	first, _ := db.Query("select id, name, price_cents, active, added from products")
	first.Next() // keeps the connection open, not closed yet

	fmt.Println("open connection count after the first query:", db.Stats().OpenConnections)

	shortCtx, cancel := context.WithTimeout(context.Background(), 0)
	defer cancel()
	_, secondErr := db.QueryContext(shortCtx, "select id, name, price_cents, active, added from products")
	fmt.Println("did the second query fail while the pool was full:", secondErr != nil)

	first.Close() // releases the connection back to the pool

	third, thirdErr := db.Query("select id, name, price_cents, active, added from products")
	fmt.Println("did the query succeed after closing:", thirdErr == nil)
	if thirdErr == nil {
		defer third.Close()
	}
}
```

```
open connection count after the first query: 1
did the second query fail while the pool was full: true
did the query succeed after closing: true
```

When the pool is limited to a single connection, that one connection stays busy for as long as
the first query's result sits unclosed. The second query is tried with a context whose timeout
expires instantly (`WithTimeout` with a zero duration) — no real duration is measured, this only
makes the "no free connection in the pool" state come back immediately as an error. When
`first.Close()` is called, the connection returns to the pool and the third query runs without
trouble. This is the concrete meaning of "the responsibility to close": for as long as a
`*sql.Rows` value stays unclosed, it keeps holding on to the connection underneath it — if the
pool's size is limited, this forgetting can block other queries. The driver itself (fake or real)
plays no role here; managing the pool is entirely `database/sql`'s own responsibility.

This measurement deliberately reduced the pool to a single connection so that the blockage would
be visible with just one query; in a pool of real size, the same forgetting might not be noticed
until multiple queries pile up. Writing `defer rows.Close()` in the code — right after the
`Query` call, before the rows are used — is the standard way to prevent this forgetting; this is
also the pattern used throughout this lesson. Closing itself produces no measurable result, it
only determines whether the next query succeeds — exactly what this measurement shows.

#### Summary

- The type set the driver carries (`driver.Value`) is limited to just seven types; whatever a
  column's "real" meaning is, it is reduced to this set when crossing the driver boundary.
- Three of the five columns (`id`, `price_cents`, `added`) are rebuilt with the target type's own
  `Scan` method; two (`name`, `active`) are assigned directly because they already overlap with
  the driver's natural type.
- Limiting measurement: scanning looks at the target's type, not the source's — an incompatible
  target fails not while the query is written, but when `Scan` is called, at run time.
- `sql.DB` manages a connection pool; for as long as a query's result stays unclosed, it keeps
  holding on to the connection underneath it.
- An unclosed row set can block later queries in a pool of limited size; closing releases the
  connection back to the pool.

#### Next Step

In this lesson, scanning rebuilt a Go value from the driver's narrow type set by looking at the
target's type — the schema sat in the code itself, in the type of the variable handed to `Scan`.
The same question was asked in five separate places across this topic's five lessons: in a file
and a buffer, in a resource embedded into a binary, in a time value turning into text, in a log
record carrying field names, and now in a database row. In all five, the value crossing the
boundary was not sufficient on its own; what made the rebuild possible was, every time, a schema
held by the other side.

Where the schema sat changed in every lesson. In the embedded resource, the schema was at compile
time — the target variable's own type. In the time value, the schema was a layout the caller
already knew. In the log record, the schema was a conversion the reading code wrote by hand. In
this lesson, the schema was the target type's own `Scan` method — bound, as a contract, to the
standard library itself. The common thread is always the same: the boundary itself never carried
the type information; what carried it was, every time, a piece of knowledge sitting on this side
of the boundary.

The next lesson closes this topic and carries the same question to the program's own entry
point: a command-line argument, too, just like a log line or a database column, arrives as text.
How many types, and how much of them, can the program's own parser rebuild from this text?

### Command-Line Applications

Source: https://academia.sh/en/courses/go-applications/command-line-applications

The last lesson of the course and the topic. Integer, boolean, duration and string types being rebuilt from a hand-built string array given to a flag parser; an unrecognized flag stopping parsing; each subcommand carrying its own flag set — all measured by running. Limiting measurement: the parser rebuilds the expected type, not the value's meaning — a negative count passes with no error. The course wrap-up gathers the boundary crossings the twelve lessons measured into a single table.

The previous lesson rebuilt a database row from the driver's narrow type set through the target
variable's own `Scan` method, closing the topic's five lessons: a file and a buffer, a resource
embedded in a binary, a time value turning into text, a log record carrying field names, a
database row — in all five, the value crossing the boundary was not sufficient on its own; the
rebuild always relied on a schema held by the other side. This lesson carries the same question
to the program's own entry point: a command-line flag, too, enters the program as text. The
Shell Programming course (M03/K02) already covered how these arguments are built on the shell
side; this lesson continues only from where the argument reaches the program's hand — once the
string is held, who rebuilds its type, with what schema?

The answer sits in this course's standard-library flag parser, called just "flag" throughout: a
command-line option that arrives as text, added in front of a call with a `-`. The parser takes a
string array and, according to flag names declared in advance in the source, produces a value in
each flag's expected type — the schema again sits on the other side, this time as declarations
written in the source itself: `set.Int`, `set.Bool`, `set.String`. This lesson measures three
things: how the parser rebuilds four types (integer, boolean, duration, string) from a string,
where an unrecognized flag stops parsing, and how the subcommand structure gives each subcommand
its own flag set. **The real argument list (`os.Args`) is never read** — the flag set is given a
hand-built string array every time, just as the fake driver in the previous lesson was given a
hand-built row. As the closing lesson of both topic and course, it also gathers the twelve
lessons' boundary crossings into a single table.

#### The Four Types the Flag Parser Rebuilds from a String

Alongside a single global flag set (`flag.CommandLine`), the flag package also supports building
your own: `flag.NewFlagSet` returns an independent set that can parse any string slice given to
it without ever touching `os.Args`. Only these independent sets are used throughout this lesson
— not a constraint, but a natural consequence of the ban on real sources: `os.Args` is a source
tied to the run's own environment, so the measurement never touches it.

**SD20.** — Four flags (integer, boolean, duration, string) are declared to a flag set; a
hand-built string array is parsed and the type of the value each flag rebuilds is read with `%T`.

```go
// types.go — the four types the flag parser rebuilds from a string
package main

import (
	"flag"
	"fmt"
	"io"
)

func main() {
	set := flag.NewFlagSet("measurement", flag.ContinueOnError)
	set.SetOutput(io.Discard)

	count := set.Int("count", 0, "count")
	keep := set.Bool("keep", false, "keep it")
	wait := set.Duration("wait", 0, "wait duration")
	name := set.String("name", "", "name")

	input := []string{"-count", "12", "-keep", "-wait", "1h30m", "-name", "bolt"}
	set.Parse(input)

	fmt.Println("-- four types rebuilt from a string --")
	fmt.Printf("%-8s source=%-7q value=%-8v type=%T\n", "count", "12", *count, *count)
	fmt.Printf("%-8s source=%-7q value=%-8v type=%T\n", "keep", "(set)", *keep, *keep)
	fmt.Printf("%-8s source=%-7q value=%-8v type=%T\n", "wait", "1h30m", *wait, *wait)
	fmt.Printf("%-8s source=%-7q value=%-8v type=%T\n", "name", "bolt", *name, *name)
}
```

```
-- four types rebuilt from a string --
count    source="12"    value=12       type=int
keep     source="(set)" value=true     type=bool
wait     source="1h30m" value=1h30m0s  type=time.Duration
name     source="bolt"  value=bolt     type=string
```

All four flags sit in the source as text — every element in `input` is a `string`, nothing else.
`set.Int` parses `"12"` into the integer behind its `*int` pointer; `set.Bool` reads a flag's
mere presence as `true`; `set.Duration` converts `"1h30m"` into an hours-and-minutes
`time.Duration`; `set.String` returns the same string unconverted. All four confirm their type
with `%T` — the parser does exactly what `Scan` did in Database Access: it rebuilds data with no
type information at the source (there the driver's narrow set, here a bare string) into a Go
type at the target, with a schema declared in advance.

This rebuild is declaration-dependent, not automatic. `set.Int("count", 0, "count")` tells the
parser in advance that `count` will carry an integer; without it the parser has no way to know
whether `"12"` is an integer or a string. Just as a struct tag carries a JSON field's name, the
schema here sits not in the source, but in the code that builds the parser.

#### What an Unrecognized Flag Does

A flag set only recognizes names declared to it in advance. When an undeclared name appears in
the string array, the parser does not silently ignore it — it **stops** parsing right there and
returns an error.

**SD21.** — After a known flag, an unrecognized flag appears, followed by a second known flag;
because parsing stops at the unrecognized flag, the second known flag is measured to never be
read.

```go
// unrecognized.go — an unrecognized flag stops parsing, nothing after it is read
package main

import (
	"flag"
	"fmt"
	"io"
)

func main() {
	set := flag.NewFlagSet("measurement", flag.ContinueOnError)
	set.SetOutput(io.Discard)

	count := set.Int("count", 0, "count")
	name := set.String("name", "initial", "name")

	input := []string{"-count", "5", "-unknown", "x", "-name", "bolt"}
	err := set.Parse(input)

	fmt.Println("-- what an unrecognized flag does --")
	fmt.Println("did parsing return an error:", err != nil)
	fmt.Println("count (read before the unrecognized flag):", *count)
	fmt.Println("name (after the unrecognized flag, never read):", *name)
}
```

```
-- what an unrecognized flag does --
did parsing return an error: true
count (read before the unrecognized flag): 5
name (after the unrecognized flag, never read): initial
```

Because `count` appears before `-unknown`, the parser reads it and assigns `5`. Reaching
`-unknown`, it finds no such flag declared in `set`, and `Parse` immediately returns an error —
the rest (`-name bolt`) is never processed; `name` stays at its default (`initial`). This is the
behavior of most of Go's own commands: parsing is never fully successful or wholly incomplete,
it stays **partial** — flags before the stopping point are read; the ones after are never seen.

`set.SetOutput(io.Discard)` is here for the same reason: in `flag.ContinueOnError` mode, the
parser normally prints its error text to a stream it writes itself, naming which flag was not
recognized. Only `err != nil` is measured; the error text itself is never written anywhere.

#### Subcommand Structure: Each Subcommand's Own Flag Set

A single flag set suffices for one flat command, but a tool with more than one sub-function
generally uses a **subcommand** structure — the first argument is a function's name, the rest
are flags specific to it. This structure is not a language feature; it is a natural consequence
of `flag.NewFlagSet` returning independent sets: each subcommand gets its own set, and a flag
declared in one never exists in the other.

**SD22.** — Two subcommands (`add`, `list`) are called with their own flag set; then `add`'s own
flag (`-count`) is handed to `list`'s set and measured to not be recognized.

```go
// subcommand.go — each subcommand carries its own flag set, one is not recognized in the other
package main

import (
	"flag"
	"fmt"
	"io"
)

func main() {
	addSet := flag.NewFlagSet("add", flag.ContinueOnError)
	addSet.SetOutput(io.Discard)
	addName := addSet.String("name", "", "product name")
	addCount := addSet.Int("count", 1, "count")

	listSet := flag.NewFlagSet("list", flag.ContinueOnError)
	listSet.SetOutput(io.Discard)
	listFilter := listSet.String("filter", "", "filter")

	commands := map[string]*flag.FlagSet{"add": addSet, "list": listSet}

	input1 := []string{"add", "-name", "bolt", "-count", "20"}
	commands[input1[0]].Parse(input1[1:])
	fmt.Println("-- subcommand: add, its own flag set --")
	fmt.Printf("name=%s count=%d\n", *addName, *addCount)

	input2 := []string{"list", "-filter", "active"}
	commands[input2[0]].Parse(input2[1:])
	fmt.Println("-- subcommand: list, its own flag set --")
	fmt.Printf("filter=%s\n", *listFilter)

	input3 := []string{"list", "-count", "5"}
	err3 := commands[input3[0]].Parse(input3[1:])
	fmt.Println("-- list never recognizes the 'count' flag in its own set --")
	fmt.Println("did it give an error:", err3 != nil)
}
```

```
-- subcommand: add, its own flag set --
name=bolt count=20
-- subcommand: list, its own flag set --
filter=active
-- list never recognizes the 'count' flag in its own set --
did it give an error: true
```

The subcommand name (`add`, `list`) is not part of any flag set — the array's first element only
selects which set gets called, and is never handed to the parser. The `add` set recognizes
`-name` and `-count`, the `list` set only `-filter`. The third call confirms this: `-count` is
valid in `add`'s set, but handed to `list`'s set the parser never recognizes it. Even defined in
the same package and `main` function, the two sets are completely independent — neither inherits
a flag the other declares.

#### Limiting Measurement: Type Is Rebuilt, Meaning Is Not

The previous three measurements showed the parser correctly rebuilding the type it expects from
a string. But "expected type" and "correct value" are not the same: the parser looks at whether
a string syntactically fits that type, never at whether the value is meaningful.

**SD23.** — The `count` flag is given a negative string (`"-3"`); the parser accepts it as a
valid integer and returns no error at all.

```go
// meaning.go — the parser rebuilds the expected type, not the value's meaning
package main

import (
	"flag"
	"fmt"
	"io"
)

func main() {
	set := flag.NewFlagSet("measurement", flag.ContinueOnError)
	set.SetOutput(io.Discard)
	count := set.Int("count", 0, "order count")

	input := []string{"-count", "-3"}
	err := set.Parse(input)

	fmt.Println("-- the parser rebuilds the expected type, not the value's meaning --")
	fmt.Println("did parsing give an error:", err != nil)
	fmt.Printf("count=%d type=%T\n", *count, *count)
}
```

```
-- the parser rebuilds the expected type, not the value's meaning --
did parsing give an error: false
count=-3 type=int
```

`"-3"` fits an integer's syntax exactly — a sign followed by digits — so `set.Int` accepts it,
and `count` takes `-3`, in `int` type, with no error. But a negative order count is invalid in
**meaning**; the parser has no knowledge of this, because its only job is converting the string
to the declared type, not enforcing a value-range rule. The same appeared in earlier lessons: a
struct tag carried a field's name but never checked its validity, a driver's scan rebuilt a
column's type but never asked whether the number inside it was meaningful. This lesson closes
that pattern — a valid-range check is the job of the code that uses the parsed value, not the
parser's.

#### Course Wrap-Up

Across twelve lessons, this course asked one question: what happens to type information as a
value crosses a boundary? The shared definition's three claims gave three answers. First: type
information is lost as a rule — in the shared measurement, seven of thirteen crossings delivered
only bytes or text to the other side, a type inside the program reduced beyond the boundary to a
number array or a line. Second: lost information can be rebuilt, but the rebuilding was, every
time, done by the side holding the schema — a struct tag, the target type itself, the type the
parser expects, the type scanning reduces to; five crossings were rebuilt this way. Third,
specific to this course: the one crossing where type information was **preserved** was the one
where both sides shared the same schema — Contract-Based Service Communication showed this in
only one of the thirteen crossings.

The twelve lessons' table is not at the same resolution as the shared measurement's thirteen
crossings: JSON Serialization alone measured four sub-crossings, and this lesson likewise
measured four types in one lesson. The direction still holds — five of the twelve rows fell into
"lost," six into "rebuilt," one into "preserved"; the majority stayed on the side of loss or
schema-based rebuilding. The limiting reading repeated in every lesson: "rebuilt" does not mean
rebuilt **correctly**, or that a meaningful value arrived. A driver's type, a struct tag's field
name, a flag parser's integer — all gave the correct type, none guaranteed the meaning the
source intended.

The course closes without overreaching. The type system only works on this side of the boundary;
on the other side — network, disk, driver, string — there is no such thing as a type, only bytes
and text. Rebuilding the schema is always the holding side's job, and when that schema is wrong
or incomplete, no warning comes: a field with no counterpart silently drops, scanning into a
wrong target fails at run time, a negative count passes unquestioned. Preservation, the
strongest outcome, depends only on both sides sharing the same schema — and that sharing was
never automatic in any lesson, it always had to be built separately.

| Lesson | Crossing measured | Outcome | Limiting measurement |
|---|---|---|---|
| HTTP Server | Whether the request's path, header and body carry type information | lost | The Content-Type header is a hint, not a type — a wrong hint produces no error on the server |
| HTTP Client | The response body being read and closed on the client side | lost | The body is a reader, not a value — a second read comes back empty with no error |
| JSON Serialization | The JSON round trip of fields marked with a struct tag | rebuilt | "Rebuilt" does not mean rebuilt correctly — a field with no counterpart silently drops |
| Templates | The same struct's output passed through two templates | lost | The field name is looked up at run time — a missing-field error is born in the middle of the output |
| Contract-Based Service Communication | The fields of a resource produced with a schema shared by two processes | preserved | Preservation depends on the schema's distribution — if the schema diverges, the crossing silently falls into lost |
| Real-Time Communication | The body of a message pushed over a long-lived connection | lost | Framing gives where the message ends, not the type of the value inside it |
| File and I/O Interfaces | Four sources (a file, a buffer, a string, a network body) passing through the same reader | lost | A partial read and the end of file are separate steps — managing the distinction is the caller's job |
| Embedded Resources | A source file binding to three Go types at compile time | rebuilt | The binary still produces the same content after the source file is deleted — the link was built at compile time |
| Time Operations | A moment turning into text and coming back | rebuilt | The offset is rebuilt but the zone name is not carried — Equal is equal, == and String() are different |
| Structured Logging | The same event written as plain text and as key-value | rebuilt | The field name is preserved, the type is not — rebuilding depends on the reader's own schema |
| Database Access | Five columns from the driver scanned into the target type | rebuilt | Scanning looks at the target's type, not the source's — an incompatible target fails at run time |
| Command-Line Applications | The string given to a flag set parsed into four types | rebuilt | The parser rebuilds the expected type, not the value's meaning — a negative count passes with no error |

#### Summary

- The flag parser rebuilt four types (integer, boolean, duration, string) from a hand-built
  string array according to previously declared flag definitions; `os.Args` was never read.
- An unrecognized flag stopped parsing right there: flags before the stopping point were read,
  the ones after were never processed.
- In the subcommand structure, each subcommand carried its own independent flag set; a flag
  declared in one was never recognized in the other.
- Limiting measurement: the parser checked whether a string syntactically fit the expected type,
  not whether the value was meaningful — a negative count passed with no error.
- Across twelve lessons, the course asked the same question at twelve boundaries and recorded
  one of three outcomes; the majority stayed lost or rebuilt, preservation appeared only once,
  where both sides shared the same schema.
- The course gathered the twelve lessons' answers into a single table; each row is read from its
  own source lesson.

