Machine generated documentation. Contribute to improve quality together.

Internals: Overview

AST Parsing of Go Source

Gohandlers uses Go’s AST packages (go/parser and go/ast) to load your project’s Go source files. It parses an entire directory of Go files into an AST, treating your existing handler functions and data structs as the “spec” for the API. In other words, your Go code (handlers and types) is the single source of truth – there’s no separate schema file to maintain. The parser walks through all top-level declarations in the package, picking out struct type definitions and function definitions that look like HTTP handlers.

After this parsing phase, the tool builds an internal model of the API:

The discovered handlers are then paired with their corresponding request/response structs by name. Gohandlers enforces a naming convention for this pairing: for a handler named X, it expects an XRequest and XResponse struct (defined in the code) to exist. The internal descriptor for a handler will link to the descriptors of its request and response binding types. (There’s logic to handle edge cases – e.g. if two handlers share a name in one file, or a handler with no request body – to ensure correct matching.)

Building Data Descriptors

Once the AST is processed, gohandlers constructs higher-level descriptor objects that drive code generation. Key components of this internal model include:

All these descriptors are stored in data structures (maps/slices) within the inspects package. For example, after inspection, you might have a map of receiver -> handlers, where each handler entry contains its HandlerInfo (with links to binding info). This structured representation of “what endpoints exist and what data they expect/produce” is now ready to feed into code generation.

Tag Extraction and Validation

A crucial part of building the descriptors is extracting struct tags and validating that they make sense. Gohandlers goes field-by-field in each binding struct and parses the tag string (the content inside backticks). It looks for known keys like route, query, json, and form. If a field has a json:"..." tag, it’s treated as part of the JSON payload; if it has query:"...", it’s marked as coming from the URL query parameters, and so on. This multi-source handling is done without runtime reflection – it’s all determined at compile time and hard-coded into the generated code. For instance, if XRequest has ID string \route:“id”` and Name string \query:“name”` defined, the generator knows that on an incoming request, ID must be pulled from the URL path and Name from the query string. The documentation confirms this: “Fields tagged route:"id" will be extracted from the URL path. Fields tagged query:"q" come from r.URL.Query(), and json:"field" from the JSON body, etc.” (gohandlers/docs/commands/bindings.md at dev · ufukty/gohandlers · GitHub). The tool ensures each field is accounted for in one of these sources. (If a field is tagged in a way gohandlers doesn’t expect, or if required tags are missing for a given context, it will likely skip or raise an error via the CLI validate command – but generally the convention is that every field in a binding struct should have a recognized tag indicating where to get/set its value.)

Struct tags vs. Content-Type: Based on the tags present, gohandlers also deduces what content types an endpoint uses. For example, if a request struct has any json:"..." fields (and no form:"..."), the request body is assumed to be JSON. Conversely, if it has form:"..." fields, it expects application/x-www-form-urlencoded form data (the tool added support for parsing form fields in addition to JSON). It will generate different code paths depending on this – e.g. calling r.ParseForm() for form data versus decoding a JSON body for JSON fields. It does not mix JSON and form in one request; one struct is generally one content type for the body. Route and query tags can coexist with either, of course. This analysis is done up front so that the code generator knows whether to include form-parsing logic, JSON unmarshaling, both, or neither for each binding. (Notably, earlier versions even had experimental support for multipart/form-data with file uploads, with tags like part and file, but that was removed as it added complexity with minimal benefit (History for pkg - ufukty/gohandlers · GitHub) (History for pkg - ufukty/gohandlers · GitHub).)

Before generation, gohandlers effectively has a complete picture of each binding struct (the “shape” of the data and where it comes from/goes) and each handler (the endpoint details and linked data types). Now it’s ready to produce code.

Template Application & Code Generation

No Reflection – Static Code: Gohandlers emphasizes type-safe, static code generation – it produces plain Go code that directly marshals/unmarshals data, instead of using reflection at runtime. This means the generation step is essentially writing out boilerplate code (like calls to strconv.Atoi, JSON encoder/decoder, etc.) tailored to your types. The generated code is placed into new Go files (by default with a .gh.go suffix). For example, running the bindings command creates a file (e.g. bindings.gh.go) in your project. This file is marked as codegen output (often you’ll see a comment at the top indicating “do not edit”).

Under the hood, gohandlers uses the collected descriptors to either fill in templates or programmatically construct Go AST nodes for the new code. In fact, much of the code generation is done by building an AST of the output file and then formatting it to source code. This approach helps manage imports and code formatting automatically. For instance, the generator will create an ast.File for the new code, set the package name to match your package, and then populate it with declarations: functions for each Parse/Build/Write method, as well as any necessary import specs (like "net/http", "encoding/json", "strconv", etc.). Using the AST, it ensures that an import is added only if needed (e.g. it adds the "net/url" import if and only if some binding has query or form fields that require URL encoding). Finally, it renders the AST to a .go file – effectively compiling the template into real Go source. The result is deterministic, properly formatted (via gofmt under the hood), and ready to compile.

Generating Parse Methods: For every binding struct, gohandlers generates a Parse method that knows how to populate that struct from an HTTP message. Request structs get a Parse(*http.Request) method, and response structs get a Parse(*http.Response) method (gohandlers/docs/commands/bindings.md at dev · ufukty/gohandlers · GitHub). These methods are essentially reading from the raw http.Request/Response and filling in the struct’s fields:

All these operations are assembled into the Parse function body. The end result is that calling, say, err := req.Parse(r) on an incoming *http.Request will fill your CreatePetRequest struct from r completely – reading path, query, form, and JSON parts as needed (gohandlers/docs/commands/bindings.md at dev · ufukty/gohandlers · GitHub). If anything is missing or invalid, the Parse method returns an error describing the problem (e.g. "invalid id parameter").

Generating Build and Write: Gohandlers also creates output methods: a Build method for request structs (to turn a struct into an outgoing *http.Request), and a Write method for response structs (to write the struct’s data into an http.ResponseWriter in a handler). These are essentially the inverse of Parse:

Additionally, for client-side usage, the tool generates a Parse method on response types as mentioned (to read *http.Response). That method would check the HTTP status and content type, then parse the body accordingly (e.g. decode JSON into the struct). This pairs with the Build method to simplify writing client libraries.

Internally, these methods are created using templates that iterate over the collected field info. Gohandlers ensures that the correct conversion or encoding logic is used for each field. Because this code is generated ahead of time, it’s fully type-checked by the compiler. There’s no runtime reflection – for example, instead of using reflect.Value to set a field, the generated code simply does req.ID = id (with id coming from strconv.Atoi(r.URL.Query().Get("id")) for instance). This approach yields zero reflection overhead and catches type errors at compile time. The maintainers describe this as “type safety, no reflection” in contrast to frameworks that might use generic maps or interface{} for handlers (GitHub - ufukty/gohandlers: Skip the boilerplate. Generate Go handler binding type parser and writer methods with type safety and zero reflection. Make sure each build registers all handlers to router. Always keep the client code of all microservices up-to-date with latest handler parameters. Go framework-less).

Finally, once all the method code is generated, gohandlers writes out the new Go file. The output file will include the package declaration, necessary import statements, and the definitions of each generated method. If you open the generated bindings.gh.go, you’ll see functions like:

func (req *CreatePetRequest) Parse(r *http.Request) error { /* ... */ }
func (req CreatePetRequest) Build(host string) (*http.Request, error) { /* ... */ }
func (res *CreatePetResponse) Parse(resp *http.Response) error { /* ... */ }
func (res CreatePetResponse) Write(w http.ResponseWriter) error { /* ... */ }

along with any helper code needed. Thanks to the structured generation, even error messages are consistent (the tool may include formatted error strings indicating which step failed, e.g., an error wrapping "ParseForm: %w" to pinpoint form parsing issues in the request pipeline).

Throughout this pipeline, key packages and types keep the process organized. The pkg/inspects package is responsible for AST parsing and building the handler/type info (types like inspects.Info for handler metadata and inspects.BindingTypeInfo for struct field data). The command implementations (e.g. cmd/gohandlers/commands/bindings.go) call into inspects to get the model, then use it to generate code (often by constructing new AST nodes for functions, statements, etc.). The result is a set of Go source files that glue your handlers and data together. In summary, gohandlers goes from parsing your source, to modeling your API’s endpoints and fields, to emitting new Go code – all in one run. It automates the boilerplate of request parsing and response writing by leveraging your code’s structure and a robust AST-driven template engine. The generated code uses your struct tags as directives for where to get data, ensuring that if you update a tag or add a new field, regenerating will update all the parsing logic accordingly (gohandlers/docs/commands/bindings.md at dev · ufukty/gohandlers · GitHub). This architecture allows your server code to truly serve as the spec for the API, with gohandlers doing the heavy lifting to keep everything in sync and type-safe.

Sources: The gohandlers README and docs provide insight into these internals and confirm the behavior of generated methods and tag usage (gohandlers/docs/commands/bindings.md at dev · ufukty/gohandlers · GitHub) (gohandlers/docs/commands/bindings.md at dev · ufukty/gohandlers · GitHub) (gohandlers/docs/commands/bindings.md at dev · ufukty/gohandlers · GitHub). The implementation uses AST parsing of Go code and comment directives (pkg/inspects: changes the gh:X directives in doc comments to be pro… · ufukty/gohandlers@4a46ecd · GitHub) to build its internal model, and then generates code accordingly, as described above. This design achieves the goal stated by the project: up-to-date handler bindings and client code with zero manual coding of boilerplate and no runtime reflection (GitHub - ufukty/gohandlers: Skip the boilerplate. Generate Go handler binding type parser and writer methods with type safety and zero reflection. Make sure each build registers all handlers to router. Always keep the client code of all microservices up-to-date with latest handler parameters. Go framework-less).