A minimal, modern AWS Lambda runtime for Go that focuses on performance, simplicity, and type safety.
Voker is a simplified alternative to aws-lambda-go that maintains full compatibility with the AWS Lambda Runtime API. It uses Go generics to provide compile-time type safety with a clean, single-function-signature design. It supports structured logging with slog and proper log levels for errors, including an optional vokerslog handler tuned for AWS Lambda.
go get github.com/hotsock/vokerpackage main
import (
"context"
"github.com/hotsock/voker"
)
type MyEvent struct {
Name string `json:"name"`
}
type MyResponse struct {
Message string `json:"message"`
}
func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
return MyResponse{
Message: "Hello, " + event.Name,
}, nil
}
func main() {
voker.Start(handler)
}func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
lc, ok := voker.FromContext(ctx)
if ok {
log.Printf("Request ID: %s", lc.AwsRequestID)
log.Printf("Function ARN: %s", lc.InvokedFunctionArn)
}
deadline, _ := ctx.Deadline()
log.Printf("Function deadline: %s", deadline)
return MyResponse{Message: "success"}, nil
}func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
if event.Name == "" {
return MyResponse{}, fmt.Errorf("name is required")
}
return MyResponse{Message: "Hello, " + event.Name}, nil
}// Handle API Gateway events
func handler(ctx context.Context, event map[string]any) (map[string]any, error) {
return map[string]any{
"statusCode": 200,
"body": "Hello from Lambda!",
}, nil
}
// Handle SQS events
type SQSEvent struct {
Records []SQSRecord `json:"Records"`
}
type SQSRecord struct {
Body string `json:"body"`
}
func handler(ctx context.Context, event SQSEvent) (string, error) {
for _, record := range event.Records {
log.Printf("Processing: %s", record.Body)
}
return "ok", nil
}Voker supports only one handler signature:
func(context.Context, TIn) (TOut, error)Where:
context.Contextis required (provides deadline, cancellation, Lambda metadata)TInis your input type (must be JSON-deserializable)TOutis your output type (must be JSON-serializable)erroris required for error handling
Declare TIn as json.RawMessage to receive the invocation payload verbatim.
Voker skips unmarshaling — and JSON validation — and hands the raw bytes
straight to your handler, which is then responsible for decoding them:
func handler(ctx context.Context, payload json.RawMessage) (Response, error) {
// payload is the raw request bytes, aliased (not copied) from the
// invocation buffer. Decode it yourself however you like.
var event MyEvent
if err := json.Unmarshal(payload, &event); err != nil {
return Response{}, err
}
// ...
}This is useful for handlers that work with large payloads and want to measure or control their own decoding rather than paying for an unmarshal up front. Because validation is skipped, the handler also sees empty or malformed payloads as-is instead of voker rejecting them.
The LambdaContext type contains metadata about the invocation:
type LambdaContext struct {
AwsRequestID string // Unique request ID
InvokedFunctionArn string // ARN of the invoked function
Identity CognitoIdentity // Cognito identity (if present)
ClientContext ClientContext // Client context (if present)
}Access it using voker.FromContext(ctx).
Voker logs with the standard library's log/slog. By default it creates a logger
from AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL using slog's built-in JSON
or text handlers. Provide your own with voker.WithLogger.
For ideal Lambda logging behavior, the optional vokerslog subpackage offers a
slog.Handler tuned for AWS Lambda advanced logging controls.
It is opt-in (import it only when you want it) and adds no extra dependency — the
request ID is read from voker.FromContext rather than aws-lambda-go.
import (
"log/slog"
"os"
"github.com/hotsock/voker"
"github.com/hotsock/voker/vokerslog"
)
func main() {
logger := slog.New(vokerslog.NewHandler(os.Stdout))
slog.SetDefault(logger)
voker.Start(handler, voker.WithLogger(logger))
}NewHandler auto-configures format (JSON or text) and level from
AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL, and enriches every record with
Lambda metadata (function name, version, and the request ID from the invocation
context). Options override the environment values:
| Option | Description |
|---|---|
WithJSON() |
Output in JSON format |
WithText() |
Output in text format |
WithLevel(slog.Leveler) |
Set the minimum log level |
WithSource() |
Include source file, function, and line number |
WithType(string) |
Set the type field (default: "app.log") |
WithoutTime() |
Omit the timestamp |
In addition to the standard slog levels, the handler maps Lambda's TRACE
(slog.LevelDebug - 4) and FATAL (slog.LevelError + 4) levels. A JSON record
looks like:
{"level":"INFO","msg":"Lambda Invoked","record":{"functionName":"my-func","version":"$LATEST","requestId":"abc-123"},"type":"app.log"}The type + record envelope mirrors the shape of AWS Lambda Telemetry API
events,
so type works best as a low-cardinality category for filtering and routing
logs (e.g. filter type = "app.request" in CloudWatch Logs Insights) rather
than for per-request data. AWS reserves function, extension, and
platform.*; a dotted app.<category> namespace avoids collisions and matches
AWS's style — for example app.log (the default), app.request, or
app.audit.
The default comes from WithType (or "app.log"). A single record can override
it with a top-level string attribute keyed vokerslog.TypeKey ("type"); the
attribute sets the record's type instead of being emitted normally. Set it via
With to tag every record from a logger, or per call:
// All records from this logger are tagged "app.request".
requests := slog.New(handler).With(vokerslog.TypeKey, "app.request")
// Or override a single record (this wins over any With value):
slog.InfoContext(ctx, "audit event", vokerslog.TypeKey, "app.audit")Setting the type to "" omits the field for that record. An attribute keyed
type inside a group is left untouched and emitted normally.
Voker automatically handles errors and panics:
func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
return MyResponse{}, errors.New("something went wrong")
}
// Returns: {"errorMessage":"something went wrong","errorType":"errorString"}func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
a := []string{"hey"}
fmt.Println(a[1]) // panic
// ...
}Returns the following (and process exits after panic):
{
"errorType": "Runtime.Panic.boundsError",
"errorMessage": "runtime error: index out of range [1] with length 1",
"stackTrace": [
{
"label": "gopanic",
"line": 783,
"path": "/usr/local/go/src/runtime/panic.go"
},
{
"label": "goPanicIndex",
"line": 115,
"path": "/usr/local/go/src/runtime/panic.go"
},
{
"label": "handler",
"line": 30,
"path": "/Users/me/Code/voker/examples/error/main.go"
},
{
"label": "callHandler[...]",
"line": 198,
"path": "Code/voker/voker.go"
},
{
"label": "handleInvocation[...]",
"line": 170,
"path": "Code/voker/voker.go"
},
{
"label": "Start[...]",
"line": 120,
"path": "Code/voker/voker.go"
},
{
"label": "main",
"line": 21,
"path": "/Users/me/Code/voker/examples/error/main.go"
},
{
"label": "main",
"line": 285,
"path": "/usr/local/go/src/runtime/proc.go"
},
{
"label": "goexit",
"line": 1268,
"path": "/usr/local/go/src/runtime/asm_arm64.s"
}
]
}func TestHandler(t *testing.T) {
event := MyEvent{Name: "World"}
response, err := handler(context.Background(), event)
assert.NoError(t, err)
assert.Equal(t, "Hello, World", response.Message)
}No mocking required - your handler is just a function!
GOOS=linux GOARCH=arm64 go build -o bootstrap main.go
zip function.zip bootstrapAWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: bootstrap
Runtime: provided.al2023
Architectures: [arm64]lambda.NewFunction(stack, jsii.String("MyFunction"), &lambda.FunctionProps{
Runtime: lambda.Runtime_PROVIDED_AL2023(),
Handler: jsii.String("bootstrap"),
Code: lambda.Code_FromAsset(jsii.String("./function.zip"), nil),
Architecture: lambda.Architecture_ARM_64(),
})import "github.com/aws/aws-lambda-go/lambda"
func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
// ...
}
func main() {
lambda.StartHandlerFunc(handler)
}import "github.com/hotsock/voker"
func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
// ...
}
func main() {
voker.Start(handler)
}That's it! If you were using the standard func(context.Context, TIn) (TOut, error) signature, it's a drop-in replacement.
If you were using lambdacontext.LambdaContext (most likely lambdacontext.FromContext(ctx) in your code, switch those to voker.FromContext(ctx)).
See LICENSE.