Getting Started with Go
This guide walks you through building a complete EvidentSource application using the Go SDK.
Prerequisites
Section titled “Prerequisites”- Go 1.22 or later
- TinyGo 0.40.1 or later (for WASM components)
- An EvidentSource server instance (local or cloud)
Quick Start with Templates
Section titled “Quick Start with Templates”The fastest way to get started is using the project template:
# Clone the SDKs repositorygit clone https://github.com/evidentsystems/evidentsource-sdkscd evidentsource-sdks/go/packages/evidentsource-functions/templates
# Create a new project./create-project.sh my-app my-database open-account active-accountsThis creates a complete project with:
- domain/ - Shared types for events and commands
- state_changes/ - Example state change component
- state_views/ - Example state view component
- build_all.sh - Build all WASM components with TinyGo
- install.sh - Deploy components to EvidentSource
- CLAUDE.md - AI assistant context for development
cd my-app./build_all.sh # Build WASM components./install.sh --all # Deploy to serverSee the Template Guide for full documentation.
Manual Project Setup
Section titled “Manual Project Setup”If you prefer to set up manually or need a client-only application:
1. Create a New Project
Section titled “1. Create a New Project”mkdir my-banking-appcd my-banking-appgo mod init github.com/myorg/banking2. Install Dependencies
Section titled “2. Install Dependencies”go get github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-corego get github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-clientgo get github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-functionsConnecting to EvidentSource
Section titled “Connecting to EvidentSource”Create main.go:
package main
import ( "context" "fmt" "log"
"github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-client" "github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-core/domain")
func main() { ctx := context.Background()
// Connect to the server es, err := client.ConnectSimple(ctx, "http://localhost:50051")Authentication
Section titled “Authentication”For production deployments, you’ll need to authenticate with a JWT token. See Authentication for details on the trust model.
import "os"
func main() { ctx := context.Background()
// Bearer token authentication (production) token := os.Getenv("EVS_TOKEN") creds := client.BearerTokenCredentials{Token: token} es, err := client.Connect(ctx, "api.example.com:50051", creds)
// DevMode for local development (no auth required) es, err := client.ConnectSimple(ctx, "http://localhost:50051") if err != nil { log.Fatal(err) } defer es.Close()
// Create or connect to a database dbName, _ := domain.NewDatabaseName("banking") conn, err := es.ConnectDatabase(ctx, dbName) if err != nil { log.Fatal(err) }
fmt.Printf("Connected to database at revision %d\n", conn.Revision())
// Execute a state change scName, _ := domain.NewStateChangeName("open-account") db, err := conn.ExecuteStateChange(ctx, scName, 1, client.JSONCommandRequest(map[string]any{ "accountId": "acct-001", "customerName": "Alice Smith", "initialDeposit": 1000.00, })) if err != nil { log.Fatal(err) }
fmt.Printf("New revision: %d\n", db.Revision())
// Query a state view svName, _ := domain.NewStateViewName("account-summary") content, err := db.ViewState(ctx, svName, 1, map[string]string{ "account_id": "acct-001", }) if err != nil { log.Fatal(err) }
if content != nil { fmt.Printf("Account data: %s\n", string(content.Content)) }}Building a State Change
Section titled “Building a State Change”State changes handle commands and emit events. Create the directory structure:
mkdir -p state-changes/open-accountCreate state-changes/open-account/main.go:
package main
import ( "encoding/json"
coredomain "github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-core/domain" sdk "github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-functions" "github.com/google/uuid")
// Commandtype OpenAccountCommand struct { AccountID string `json:"accountId"` CustomerName string `json:"customerName"` InitialDeposit float64 `json:"initialDeposit"`}
// Eventstype AccountOpened struct { AccountID string `json:"accountId"` CustomerName string `json:"customerName"`}
type AccountCredited struct { Amount float64 `json:"amount"` Description string `json:"description"`}
func decide(db sdk.DatabaseAccess, cmd OpenAccountCommand, meta sdk.StateChangeMetadata) (sdk.DecideResult, error) { // Validate if cmd.CustomerName == "" { return sdk.DecideResult{}, sdk.ValidationErr("customer name is required") } if cmd.InitialDeposit < 0 { return sdk.DecideResult{}, sdk.ValidationErr("initial deposit cannot be negative") }
// Check if account already exists existing, _ := db.ViewState("account-summary", 1, map[string]string{ "account_id": cmd.AccountID, }) if existing != nil { return sdk.DecideResult{}, sdk.ConflictErr("account already exists") }
var events []coredomain.ProspectiveEvent
// Create AccountOpened event openedData, _ := json.Marshal(AccountOpened{ AccountID: cmd.AccountID, CustomerName: cmd.CustomerName, }) events = append(events, coredomain.ProspectiveEvent{ ID: uuid.New().String(), Stream: "accounts", EventType: "com.banking.account.opened", Subject: &cmd.AccountID, Data: coredomain.StringEventData{Data: string(openedData)}, DataContentType: ptr("application/json"), })
// Create initial deposit event if non-zero if cmd.InitialDeposit > 0 { creditData, _ := json.Marshal(AccountCredited{ Amount: cmd.InitialDeposit, Description: "Initial deposit", }) events = append(events, coredomain.ProspectiveEvent{ ID: uuid.New().String(), Stream: "accounts", EventType: "com.banking.account.credited", Subject: &cmd.AccountID, Data: coredomain.StringEventData{Data: string(creditData)}, DataContentType: ptr("application/json"), }) }
return sdk.DecideResult{Events: events}, nil}
func ptr(s string) *string { return &s }
var Adapter = sdk.StateChangeAdapter[OpenAccountCommand]{ ParseCommand: sdk.JSONCommandParser[OpenAccountCommand](), Decide: decide,}
func main() {}Create state-changes/open-account/go.mod:
module github.com/myorg/banking/state-changes/open-account
go 1.22Build the WASM component:
cd state-changes/open-accounttinygo build -o open-account.wasm -target=wasip2 -scheduler=none .Building a State View
Section titled “Building a State View”State views materialize read models from events. Create the directory structure:
mkdir -p state-views/account-summaryCreate state-views/account-summary/main.go:
package main
import ( "encoding/json"
sdk "github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-functions")
// Statetype AccountSummary struct { ID string `json:"id"` Name string `json:"name"` Balance float64 `json:"balance"` IsOpen bool `json:"isOpen"`}
// Event data typestype AccountOpenedData struct { AccountID string `json:"accountId"` CustomerName string `json:"customerName"`}
type AccountCreditedData struct { Amount float64 `json:"amount"`}
type AccountDebitedData struct { Amount float64 `json:"amount"`}
type AccountSummaryView struct{}
func (v *AccountSummaryView) InitialState() AccountSummary { return AccountSummary{}}
func (v *AccountSummaryView) EvolveEvent(state AccountSummary, event sdk.Event) AccountSummary { switch event.EventType { case "com.banking.account.opened": var data AccountOpenedData if err := json.Unmarshal(event.Data, &data); err == nil { state.ID = data.AccountID state.Name = data.CustomerName state.IsOpen = true } case "com.banking.account.credited": var data AccountCreditedData if err := json.Unmarshal(event.Data, &data); err == nil { state.Balance += data.Amount } case "com.banking.account.debited": var data AccountDebitedData if err := json.Unmarshal(event.Data, &data); err == nil { state.Balance -= data.Amount } case "com.banking.account.closed": state.IsOpen = false } return state}
var Adapter = sdk.StateViewAdapter[AccountSummary]{ View: &AccountSummaryView{},}
func main() {}Build the WASM component:
cd state-views/account-summarytinygo build -o account-summary.wasm -target=wasip2 -scheduler=none .Deploying Components
Section titled “Deploying Components”Deploy your WASM components to the EvidentSource server:
# Create databasecurl -X POST http://localhost:8080/api/v1/databases/banking
# Install state changecurl -X POST http://localhost:8080/api/v1/databases/banking/state-changes/open-account/versions/1 \ --data-binary @state-changes/open-account/open-account.wasm \ -H "Content-Type: application/wasm"
# Install state viewcurl -X POST http://localhost:8080/api/v1/databases/banking/state-views/account-summary/versions/1 \ --data-binary @state-views/account-summary/account-summary.wasm \ -H "Content-Type: application/wasm"Testing
Section titled “Testing”Use the mock database for unit testing:
package main
import ( "testing"
functionstest "github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-functions/testing")
func TestOpenAccountSuccess(t *testing.T) { db := functionstest.NewMockDatabase("test-db").WithRevision(100)
cmd := OpenAccountCommand{ AccountID: "acct-001", CustomerName: "Alice", InitialDeposit: 100.0, }
result, err := decide(db, cmd, sdk.StateChangeMetadata{}) if err != nil { t.Fatalf("unexpected error: %v", err) }
if len(result.Events) != 2 { t.Errorf("expected 2 events, got %d", len(result.Events)) }}
func TestOpenAccountNegativeDeposit(t *testing.T) { db := functionstest.NewMockDatabase("test-db")
cmd := OpenAccountCommand{ AccountID: "acct-001", CustomerName: "Alice", InitialDeposit: -50.0, }
_, err := decide(db, cmd, sdk.StateChangeMetadata{}) if err == nil { t.Error("expected validation error") }}Next Steps
Section titled “Next Steps”- Explore the Savings Account example for a complete banking application
- Read the WASM Building Guide for TinyGo tips
- Learn about bi-temporal queries for historical analysis