中文文档: README_CN.md
This library is optimized around low-latency request handling, tight routing, and low-allocation parsing/writing paths.
Current benchmark snapshot on darwin/arm64 (Apple M2):
| Benchmark | Result | Memory |
|---|---|---|
BenchmarkServeHTTPStaticJSON |
138.7 ns/op |
16 B/op, 1 alloc/op |
BenchmarkServeHTTPPathParamJSON |
182.9 ns/op |
24 B/op, 2 alloc/op |
BenchmarkServeHTTPNoContent |
19.8 ns/op |
0 B/op, 0 alloc/op |
BenchmarkServeHTTPManualWrite |
21.4 ns/op |
0 B/op, 0 alloc/op |
BenchmarkServeHTTPStandardHandler |
22.9 ns/op |
0 B/op, 0 alloc/op |
BenchmarkServeHTTPBlob |
69.9 ns/op |
16 B/op, 1 alloc/op |
BenchmarkServeHTTPStaticJSONRawMessage |
109.2 ns/op |
40 B/op, 2 alloc/op |
BenchmarkTryParseJSONBodyFast |
1392.6 ns/op |
5599 B/op, 20 alloc/op |
BenchmarkServeHTTPBinary |
113.0 ns/op |
40 B/op, 2 alloc/op |
BenchmarkServeHTTPAvro |
113.1 ns/op |
40 B/op, 2 alloc/op |
BenchmarkTreeGetValueParamPooled |
14.7 ns/op |
0 B/op, 0 alloc/op |
BenchmarkCtxParamUint64 |
10.8 ns/op |
0 B/op, 0 alloc/op |
BenchmarkTryParseInt64 |
10.6 ns/op |
0 B/op, 0 alloc/op |
BenchmarkTryParseUint64 |
10.0 ns/op |
0 B/op, 0 alloc/op |
BenchmarkTryParseIntSlice |
32.7 ns/op |
0 B/op, 0 alloc/op |
BenchmarkTryParseStringSlice |
23.0 ns/op |
0 B/op, 0 alloc/op |
BenchmarkParseMediaTypeExactJSON |
1.9 ns/op |
0 B/op, 0 alloc/op |
BenchmarkAcceptMediaTypeEmpty |
2.0 ns/op |
0 B/op, 0 alloc/op |
Notes:
Memoryreports Go benchmarkB/opandallocs/op; the snapshot was collected with-benchmem.- Static JSON responses are down to a single allocation on the request path.
- No-content and manual-write response paths stay at
0 alloc. - Standard
net/httphandlers can be mounted with0 allocrequest overhead. Ctx.Blobprovides an explicit fast path for pre-encoded byte responses.- Param and catch-all routing become
0 allocwhen params are pooled, which is already howApplicationruns. - Pre-encoded JSON (
json.RawMessage) has a dedicated write fast path. TryParseJSONBodyFastis the opt-in fast path for JSON request bodies when unknown-field rejection is not required.- Client response decoding has an explicit raw-body fast path via
*web.RawBody. - Binary and avro responses have direct fast paths.
- Integer and slice parsing hot paths avoid extra scans and intermediate slices while remaining
0 alloc.
Run the current benchmark suite:
go test -run '^$' -bench 'Benchmark(ServeHTTP|TreeGetValue|TryParse|TryInt|TryUint|TryBool|Post(JSON|Bytes)|DoReqWithClient(Struct|RawBody)|Ctx|ParamsVal|ParseMediaType|AcceptMediaType)' -benchmem ./...Compare current results against the committed baseline:
./bench/compare.shRefresh the committed baseline:
./bench/update_baseline.shGenerate a Markdown benchmark snapshot ready to paste into the README:
./bench/snapshot.shUpdate the benchmark snapshot blocks in README.md and README_CN.md:
./bench/update_snapshot_readme.shUseful overrides:
COUNT=3 ./bench/compare.sh
BENCH_EXPR='BenchmarkServeHTTP(StaticJSON|PathParamJSON)$' ./bench/compare.sh
CURRENT_FILE=./bench/servehttp.txt COUNT=3 ./bench/compare.sh
SHOW_MISSING=1 ./bench/compare.sh
COUNT=3 ./bench/update_baseline.sh
COUNT=3 ./bench/snapshot.sh
COUNT=3 ./bench/update_snapshot_readme.shFiles:
- baseline: bench/baseline.txt
- compare script: bench/compare.sh
- update script: bench/update_baseline.sh
- snapshot script: bench/snapshot.sh
- README update script: bench/update_snapshot_readme.sh
- Prefer
[]byteorweb.AvroMarshalerfor binary/avro responses. - Prefer
c.Blob(...)for pre-encoded byte responses that should write immediately. - Use
HandleHTTP/GetHTTP/PostHTTPwhen integrating existingnet/httphandlers. - Prefer
PostBytes/PutBytes/PatchBytes/DoByteswhen the request body is already encoded. - Prefer
*WithClienthelpers when you need tuned timeouts, connection pooling, or a custom transport. - Reuse destination slices when calling
TryParse(..., &slice)in hot paths. - Prefer pooled param paths if you benchmark routing in isolation; the framework already does this in normal request handling.
- Treat single benchmark runs as noisy. Use the baseline comparison script for direction, not intuition.
package main
import (
"log"
"net/http"
"pkg.gostartkit.com/web"
)
func main() {
app := web.New()
app.Get("/health", func(c *web.Ctx) (any, error) {
return map[string]string{"status": "ok"}, nil
})
log.Fatal(app.ListenAndServe("tcp", ":8080"))
}This section focuses on the most common way to use the framework. If you only need to get an API online quickly, start here.
Use web.New() for the default setup. If you already know you want middleware
or structured errors, pass options at construction time.
app := web.New(
web.WithMiddleware(
web.RequestID("", nil),
web.Recover(nil),
),
web.WithErrorHandler(web.JSONErrorHandler(true)),
)Use the option-based form when you want startup code to stay declarative. Use
app.Use(...) when middleware is added later or conditionally.
Handlers use the signature:
func(c *web.Ctx) (any, error)The simplest handler returns a value and lets the framework encode it.
app.Get("/health", func(c *web.Ctx) (any, error) {
return map[string]string{"status": "ok"}, nil
})You can also register other HTTP methods:
app.Post("/users", createUser)
app.Put("/users/:id", updateUser)
app.Delete("/users/:id", deleteUser)
app.Handle("PURGE", "/cache/:key", purgeCache)If you already have standard net/http handlers, mount them directly:
app.GetHTTP("/metrics", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))Use Param, Query, and Form for string access. Use typed helpers when you
want parsing and validation in one step.
app.Get("/users/:id", func(c *web.Ctx) (any, error) {
id, err := c.ParamUint64("id")
if err != nil {
return nil, web.ErrBadRequest
}
verbose := c.Query("verbose") == "1"
return map[string]any{
"id": id,
"verbose": verbose,
}, nil
})For form requests:
app.Post("/login", func(c *web.Ctx) (any, error) {
email := c.Form("email")
password := c.Form("password")
if email == "" || password == "" {
return nil, web.ErrBadRequest
}
return map[string]bool{"ok": true}, nil
})Use TryParseBody when the request Content-Type should control decoding. It
supports the built-in JSON, GOB, and XML readers.
type CreateUserRequest struct {
Name string `json:"name"`
Age int `json:"age"`
}
app.Post("/users", func(c *web.Ctx) (any, error) {
var req CreateUserRequest
if err := c.TryParseBody(&req); err != nil {
return nil, err
}
return map[string]any{
"name": req.Name,
"age": req.Age,
}, nil
})Use TryParseJSONBodyFast when you know the body is JSON and want the faster
path based on pooled buffers and json.Unmarshal.
app.Post("/events", func(c *web.Ctx) (any, error) {
var req struct {
Type string `json:"type"`
}
if err := c.TryParseJSONBodyFast(&req); err != nil {
return nil, err
}
return map[string]string{"accepted": req.Type}, nil
})Choose TryParseBody when strict JSON unknown-field rejection matters. Choose
TryParseJSONBodyFast when raw speed matters more.
The default response behavior is intentionally simple:
return nil, nilwrites204 No Contentreturn value, nilwrites200 OKc.SetStatus(code)overrides the default success statusreturn nil, errwrites an error response
Example with explicit success status:
app.Post("/users", func(c *web.Ctx) (any, error) {
c.SetStatus(http.StatusCreated)
return map[string]string{"result": "created"}, nil
})If you want to write the response immediately, use the explicit helpers:
app.Get("/ping", func(c *web.Ctx) (any, error) {
return nil, c.String(http.StatusOK, "pong")
})
app.Get("/config.json", func(c *web.Ctx) (any, error) {
return nil, c.JSON(http.StatusOK, map[string]bool{"ok": true})
})
app.Get("/logo", func(c *web.Ctx) (any, error) {
return nil, c.Blob(http.StatusOK, "image/png", pngBytes)
})For framework-style handlers, returning values is usually the more idiomatic choice. The immediate helpers are best when you need exact response control.
Use groups to keep prefixes and middleware close to the routes that need them.
api := app.Group("/api", web.Timeout(2*time.Second))
api.Use(web.AccessLog(func(c *web.Ctx, status int, d time.Duration, err error) {
log.Printf("%s %s -> %d (%s)", c.Method(), c.Path(), status, d)
}))
api.Get("/users/:id", func(c *web.Ctx) (any, error) {
return map[string]string{
"id": c.Param("id"),
"request_id": c.RequestID(),
}, nil
})The middleware order is:
- application middleware
- parent group middleware
- child group middleware
- route middleware
Common built-in middleware you can mix in:
web.RequestID("", nil)adds a request ID to context and response headersweb.Recover(nil)converts panics into framework errorsweb.Timeout(2 * time.Second)adds a cooperative deadlineweb.MaxBodyBytes(1 << 20)limits request bodies to 1 MiBweb.SecurityHeaders()adds a default set of security response headersweb.CORSMiddleware(...)emits CORS headers for matched routes
You can return the built-in framework errors directly:
app.Get("/private", func(c *web.Ctx) (any, error) {
if c.BearerToken() == "" {
return nil, web.ErrUnauthorized
}
return map[string]bool{"ok": true}, nil
})To standardize API error output, install JSONErrorHandler:
app.SetErrorHandler(web.JSONErrorHandler(true))That produces a body like:
{
"code": 401,
"message": "UNAUTHORIZED",
"request_id": "abc123"
}For redirects, prefer c.Redirect(...):
app.Get("/old-home", func(c *web.Ctx) (any, error) {
return nil, c.Redirect(http.StatusMovedPermanently, "/new-home")
})Use ServeFiles when part of your application should expose files from a
directory.
app.ServeFiles("/static/*filepath", http.Dir("./public"))Requests such as /static/app.css or /static/js/app.js are forwarded to the
underlying file system with the /static prefix stripped.
For browser-facing APIs, a common setup looks like this:
app := web.New(
web.WithCORS(web.NewCORS(web.CORSOptions{
AllowOrigins: []string{"https://app.example.com"},
AllowHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 10 * time.Minute,
})),
)
app.Use(
web.CORSMiddleware(web.CORSOptions{
AllowOrigins: []string{"https://app.example.com"},
ExposeHeaders: []string{"X-Request-Id"},
AllowCredentials: true,
}),
web.SecurityHeaders(),
)Use both pieces together when you want:
NewCORS(...)to cover the framework's automaticOPTIONSresponsesCORSMiddleware(...)to cover matchedGET/POST/PUT/PATCH/DELETEresponses
If you only need security headers, web.SecurityHeaders() is the shortest path.
Use SecurityHeadersWithOptions(...) when you need custom CSP, HSTS, or related
policies.
The package also includes lightweight client helpers.
Simple JSON GET:
var resp struct {
Name string `json:"name"`
}
if err := web.Get(context.Background(), "https://api.example.com/user/1", "", &resp); err != nil {
return err
}JSON POST:
payload := map[string]string{"name": "alice"}
var resp struct {
ID uint64 `json:"id"`
}
if err := web.Post(context.Background(), "https://api.example.com/users", token, payload, &resp); err != nil {
return err
}Pre-encoded request body:
body := []byte(`{"name":"alice"}`)
if err := web.PostBytes(context.Background(), "https://api.example.com/users", token, body, &resp); err != nil {
return err
}Retrying requests:
if err := web.TryGet(context.Background(), "https://api.example.com/user/1", token, &resp, 3); err != nil {
return err
}Use *WithClient helpers when you need a custom timeout, transport, or
connection pool:
client := &http.Client{Timeout: 2 * time.Second}
if err := web.GetWithClient(client, context.Background(), "https://api.example.com/user/1", token, &resp); err != nil {
return err
}If you want the raw response bytes instead of JSON decoding:
req, _ := http.NewRequest(http.MethodGet, "https://example.com/data", nil)
var raw web.RawBody
if err := web.DoReqWithClient(http.DefaultClient, req, &raw, nil); err != nil {
return err
}web.New(options ...Option) *Application- route registration:
Get,Post,Put,Patch,Delete,Head,Options,HandleGetHTTP,PostHTTP,PutHTTP,PatchHTTP,DeleteHTTP,HeadHTTP,OptionsHTTP,HandleHTTP
- framework composition:
Use,Group,SetErrorHandler,RegisterReader,RegisterWriter- options:
WithMiddleware,WithErrorHandler,WithNotFound,WithMethodNotAllowed,WithCORS
- server lifecycle:
ListenAndServe,ListenAndServeTLS,Shutdown
- helpers:
ServeFiles,Redirect,TryParse(...),TryXxx(...),JSONErrorHandler,NewCORS
- context (
*Ctx) common methods:- request:
Method,Path,Query,Param,Body,ContentType,BearerToken,RequestID - parse:
TryParseBody,TryParseJSONBodyFast,TryParseParam,TryParseQuery,TryParseForm - response:
SetHeader,SetCookie,AllowCredentials,JSON,String,Blob,NoContent, content negotiation viaAccept
- request:
| Area | API | Description |
|---|---|---|
| Application | New() |
Create app instance |
| Application | New(WithMiddleware(...), WithErrorHandler(...)) |
Create an app with construction-time options |
| Application | Get/Post/Put/Patch/Delete/Head/Options(path, handler) |
Register route handler |
| Application | Handle(method, path, handler) |
Register route handler for an arbitrary HTTP method |
| Application | GetHTTP/PostHTTP/.../HandleHTTP(path, http.Handler) |
Mount standard net/http handlers |
| Application | Use(middleware...) |
Apply app-level middleware to subsequently registered routes |
| Application | Group(prefix, middleware...) |
Create route groups with shared prefix and middleware |
| Application | SetErrorHandler(handler) |
Install a custom route error handler |
| Application | SetCORS(cors) |
Install a CORS hook for automatic OPTIONS responses |
| Application | RegisterReader(contentType, reader) |
Override request decoding for a media type |
| Application | RegisterWriter(contentType, writer) |
Override response encoding for a media type |
| Application | ServeFiles("/static/*filepath", fs) |
Serve static files with catch-all path |
| Application | ListenAndServe(network, addr, ...opts) |
Start HTTP server |
| Application | ListenAndServeTLS(network, addr, tlsConfig, ...opts) |
Start HTTPS server |
| Application | Shutdown(ctx) |
Graceful shutdown |
| Context | Param(name), Query(name), Form(name), RequestID() |
Read path/query/form values and middleware-provided request ID |
| Context | TryParseBody(v) |
Parse request body by content type (JSON/GOB/XML) |
| Context | TryParseJSONBodyFast(v) |
Fast JSON body parse using pooled buffer + json.Unmarshal |
| Context | TryParseParam/Query/Form(name, &v) |
Parse string values into typed value |
| Context | SetHeader, SetCookie, SetContentType, SetStatus |
Write response headers and override the default success status |
| Context | JSON, String, Blob, NoContent |
Immediate response helpers for explicit writes |
| Context | Request(), ResponseWriter(), Context() |
Access raw HTTP objects |
| Middleware | RequestID, Recover, RecoverWithOptions, Timeout, AccessLog, AccessLogWithOptions |
Core built-in opt-in middleware helpers |
| Middleware | MaxBodyBytes(limit) |
Limit request body size |
| Middleware | SecurityHeaders() / SecurityHeadersWithOptions(...) |
Add common security response headers |
| Middleware | CORSMiddleware(CORSOptions) |
Emit CORS headers on matched route responses |
| CORS helper | NewCORS(CORSOptions) |
Create a CORS hook for automatic OPTIONS handling |
| Client | Get/Post/Put/Patch/Delete/Do |
HTTP client helpers using http.DefaultClient |
| Client | GetWithClient/PostWithClient/PutWithClient/PatchWithClient/DeleteWithClient/DoWithClient |
HTTP helpers with explicit *http.Client |
| Client | DoReq/DoReqWithClient |
Execute prepared requests and decode JSON or RawBody responses |
| Client | PostBytes/PutBytes/PatchBytes/DoBytes |
Send pre-encoded request bodies without JSON encoding |
| Client | PostBytesWithClient/PutBytesWithClient/PatchBytesWithClient/DoBytesWithClient |
Pre-encoded body helpers with explicit *http.Client |
| Client | TryGet/TryPost/TryPut/TryPatch/TryDelete/TryDo |
HTTP helpers with retry loop |
| Client | TryGetWithClient/TryPostWithClient/TryPutWithClient/TryPatchWithClient/TryDeleteWithClient/TryDoWithClient |
Retry helpers with explicit *http.Client |
| Client | TryPostBytes/TryPutBytes/TryPatchBytes/TryDoBytes |
Retry-capable helpers for pre-encoded request bodies |
| Client | TryPostBytesWithClient/TryPutBytesWithClient/TryPatchBytesWithClient/TryDoBytesWithClient |
Retry-capable pre-encoded helpers with explicit *http.Client |
| Error | NewErr(code, msg) |
Error with HTTP status code |
| Error | Redirect(url, code) |
Return redirect response from handler |
| Error | JSONErrorHandler(includeRequestID) |
Write structured JSON API errors |
- Handler return value controls response:
(nil, nil)->204 No Content(value, nil)->200 OK- call
c.SetStatus(code)to explicitly override the default success status (_, err)-> status code from framework error type, body containserr.Error()
- Response format is selected by request
Acceptheader:application/jsonapplication/x-gobapplication/xmlapplication/octet-streamapplication/x-avro
- Static, param, and catch-all segments can coexist at the same tree level.
- Match priority is fixed and does not depend on registration order:
static > param > catch-all
- Catch-all routes are still only allowed at the end of the path.
- Invalid wildcard combinations remain rejected at registration time.
This makes common REST route sets work without handler-level catch-all dispatch:
app.Get("/organizations/:id/devices/bulk/disable", bulkDisable)
app.Get("/organizations/:id/devices/provision", provision)
app.Get("/organizations/:id/devices/config/rollout", configRollout)
app.Get("/organizations/:id/devices/:device_id", showDevice)With the routes above:
GET /organizations/1/devices/provisionmatches the static route.GET /organizations/1/devices/42matches the param route.- Registering
:device_idbefore or afterprovisionproduces the same result.
- Middleware and route groups are registration-time features:
app.Use(...)app.Group("/api", ...)- group-local
Use(...)
- Built-in middleware is explicit opt-in:
RequestIDRecoverRecoverWithOptionsTimeoutAccessLogAccessLogWithOptionsMaxBodyBytesSecurityHeadersSecurityHeadersWithOptionsCORSMiddleware
- Structured API errors are opt-in via
SetErrorHandler(JSONErrorHandler(...)) - Automatic
OPTIONSCORS responses are opt-in viaSetCORS(NewCORS(...)) - Reader/writer overrides are media-type specific and do not affect the default hot path unless registered
- Construction-time options let setup stay declarative without changing request-time cost.
- Existing
net/httphandlers can be mounted directly withHandleHTTP/GetHTTP.
app := web.New(
web.WithMiddleware(web.RequestID("", nil), web.Recover(nil)),
web.WithErrorHandler(web.JSONErrorHandler(true)),
)
api := app.Group("/api", web.Timeout(2*time.Second))
api.Get("/users/:id", func(c *web.Ctx) (any, error) {
return map[string]string{
"id": c.Param("id"),
"request_id": c.RequestID(),
}, nil
})
api.GetHTTP("/metrics", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
api.Get("/avatar/:id", func(c *web.Ctx) (any, error) {
return nil, c.Blob(http.StatusOK, "image/png", []byte{0x89, 'P', 'N', 'G'})
})For finer control, use the options-based middleware variants:
app.Use(
web.RecoverWithOptions(web.RecoverOptions{
DefaultStatus: http.StatusServiceUnavailable,
DefaultBody: "UNAVAILABLE",
}),
web.AccessLogWithOptions(web.AccessLogOptions{
Log: func(c *web.Ctx, entry web.AccessLogEntry) {
// route-aware access logging hook
},
}),
)Try*retry semantics updated:retry <= 0now still performs one request attempt.- retry loop stops early for
ErrUnauthorized,ErrForbidden, andErrBadRequest(including wrapped).
TryDonow supports safe body replay across retries (request body is buffered once and recreated per attempt).- Raw body helpers added:
PostBytes,PutBytes,PatchBytes,DoBytesTryPostBytes,TryPutBytes,TryPatchBytes,TryDoBytes- default request headers are
Content-Type: application/octet-streamandAccept: application/json
- Explicit client helpers added:
DoReqWithClient,DoWithClient,DoBytesWithClient- wrapper and retry variants for
Get/Post/Put/Patch/Delete - use these when transport-level performance tuning matters
- Raw response fast path added:
DoReq/DoReqWithClientnow recognize*web.RawBody- existing JSON destinations like
[]byteandjson.RawMessagekeep their original JSON semantics
Ctx.writeBinaryandCtx.writeAvroare implemented:- previous behavior for these media types was
ErrNotImplemented. - now they support fast-path direct writing (see Binary / Avro response section).
- previous behavior for these media types was
- Redirect usage:
- returning only
ErrMovedPermanentlydoes not setLocation. - use
web.Redirect(url, code)to generate proper redirect response headers.
- returning only
- Header negotiation improvement:
Accept/Content-Typevalues with parameters (e.g.application/json; charset=utf-8) are now parsed correctly.
Migration tips:
- If you relied on
retry=0to skip outbound call, replace with explicit conditional in caller. - If your handlers used
application/octet-streamorapplication/x-avro, you can now return[]byte,io.Reader, or custom marshaler types directly. - For redirects, migrate to
web.Redirect(...)for predictable behavior.
Ctx.writeBinary and Ctx.writeAvro are optimized for fast paths.
- Binary fast-path input types:
[]bytestring*bytes.Bufferio.Readerencoding.BinaryMarshaler
- Avro fast-path input types:
web.AvroMarshaler- falls back to binary writer for the same input types above
type Event struct {
Raw []byte
}
func (e Event) MarshalAvro() ([]byte, error) {
return e.Raw, nil
}
app.Get("/payload", func(c *web.Ctx) (any, error) {
// Client sends: Accept: application/x-avro
return Event{Raw: []byte{0xAA, 0xBB}}, nil
})Use web.Redirect(url, code) to return redirect responses.
app.Get("/old", func(c *web.Ctx) (any, error) {
return web.Redirect("/new", http.StatusMovedPermanently)
})TryGet, TryPost, TryPut, TryPatch, TryDelete, TryDo:
retry <= 0still performs at least one request.- retries stop early for non-retriable errors:
ErrUnauthorizedErrForbiddenErrBadRequest(including wrapped)
TryDosafely retries with request body replay (body is cached once and recreated per attempt).
Use TryParseJSONBodyFast when the request body is JSON and unknown-field rejection is not required.
app.Post("/ingest", func(c *web.Ctx) (any, error) {
var req struct {
ID int `json:"id"`
}
if err := c.TryParseJSONBodyFast(&req); err != nil {
return nil, err
}
return struct {
Ok bool `json:"ok"`
}{Ok: true}, nil
})Use DoReqWithClient with *web.RawBody when you want the response payload without JSON decoding cost.
req, _ := http.NewRequest(http.MethodGet, "https://example.com/data", nil)
var raw web.RawBody
if err := web.DoReqWithClient(client, req, &raw, nil); err != nil {
panic(err)
}- Best performance for param/catch-all routing is achieved when params are pooled (already used in
Application). - For binary/avro responses, prefer returning
[]byteor implementingweb.AvroMarshalerto avoid extra encoding overhead. TryParseBodycurrently supports JSON/GOB/XML only.
Thanks to all open-source projects, I’ve learned a lot from them.
Special thanks to:
- httprouter: A high-performance HTTP router that inspired the routing logic in this project.
- web: A lightweight web framework that provided insights into efficient server design.