Dataset Composition
The pkg/datasets package provides four complementary APIs for constructing and combining datasets without touching raw DataDefinition slices directly: Clone, Merge, Builder, and the global Registry.
Clone
Clone returns an independent deep copy of a DataSet value.
import "github.com/mrlm-net/simconnect/pkg/datasets"
original := datasets.DataSet{
Definitions: []datasets.DataDefinition{
{Name: "PLANE LATITUDE", Unit: "degrees", Type: types.SIMCONNECT_DATATYPE_FLOAT64},
{Name: "PLANE LONGITUDE", Unit: "degrees", Type: types.SIMCONNECT_DATATYPE_FLOAT64},
},
}
clone := original.Clone()
Immutability contract
DataSet is a value type — it holds a slice header, not a pointer. Assigning one DataSet to another copies the header (pointer, length, capacity) but not the backing array, so both variables still share the same underlying memory. Clone allocates a fresh backing array and copies all elements into it, breaking that shared reference.
After Clone, appending to or modifying a field in clone.Definitions has no effect on original, and vice versa.
When to use Clone
Use Clone when you need to hand a dataset to code that may mutate it (for example, passing it to a library that appends or reorders definitions) while keeping the source dataset unchanged. Also use it when storing a snapshot of a dataset for later comparison or rollback.
Note: because Clone is a value receiver method, you can call it on any DataSet — including ones returned from constructor functions — without a pointer:
ds := traffic.NewAircraftDataset() // *DataSet
snapshot := ds.Clone() // independent copy
Merge
Merge combines any number of DataSet arguments into a single new DataSet, with last-wins deduplication by Name.
merged := datasets.Merge(baseDS, extensionDS)
Deduplication and position shift
When the same Name appears in more than one input dataset, the earlier occurrence is removed and the later occurrence is placed at the position it was last seen. Relative order among surviving (non-duplicate) entries is preserved.
Example:
Input A: [PLANE LATITUDE, PLANE LONGITUDE, PLANE ALTITUDE]
Input B: [PLANE LONGITUDE, AIRSPEED INDICATED]
Result: [PLANE LATITUDE, PLANE ALTITUDE, PLANE LONGITUDE, AIRSPEED INDICATED]
PLANE LONGITUDE from A is removed because it reappears in B; the B copy occupies the position of its last occurrence (after PLANE ALTITUDE).
Zero and single argument behaviour
Merge()with zero arguments returns an emptyDataSet.Merge(ds)with a single argument is equivalent tods.Clone()— it returns a fresh independent copy.
Independence guarantee
The returned DataSet always owns its own backing array. Mutations to the result do not affect any of the input datasets.
a := datasets.DataSet{Definitions: []datasets.DataDefinition{
{Name: "PLANE LATITUDE", Unit: "degrees", Type: types.SIMCONNECT_DATATYPE_FLOAT64},
{Name: "PLANE LONGITUDE", Unit: "degrees", Type: types.SIMCONNECT_DATATYPE_FLOAT64},
{Name: "PLANE ALTITUDE", Unit: "feet", Type: types.SIMCONNECT_DATATYPE_FLOAT64},
}}
b := datasets.DataSet{Definitions: []datasets.DataDefinition{
{Name: "PLANE LONGITUDE", Unit: "degrees", Type: types.SIMCONNECT_DATATYPE_FLOAT64},
{Name: "AIRSPEED INDICATED", Unit: "knots", Type: types.SIMCONNECT_DATATYPE_FLOAT64},
}}
result := datasets.Merge(a, b)
// result.Definitions: [PLANE LATITUDE, PLANE ALTITUDE, PLANE LONGITUDE, AIRSPEED INDICATED]
Builder
Builder provides a fluent API for constructing DataSet values incrementally. It is useful when you want to assemble a dataset programmatically or extend an existing one without mutating the source.
import "github.com/mrlm-net/simconnect/pkg/datasets"
import "github.com/mrlm-net/simconnect/pkg/types"
ds := datasets.NewBuilder().
AddField("PLANE LATITUDE", "degrees", types.SIMCONNECT_DATATYPE_FLOAT64, 0).
AddField("PLANE LONGITUDE", "degrees", types.SIMCONNECT_DATATYPE_FLOAT64, 0).
AddField("PLANE ALTITUDE", "feet", types.SIMCONNECT_DATATYPE_FLOAT64, 0).
Build()
AddField vs Add
AddField is the convenience form — it takes individual parameters and constructs the DataDefinition internally:
b.AddField("PLANE LATITUDE", "degrees", types.SIMCONNECT_DATATYPE_FLOAT64, 0)
Add accepts a pre-built DataDefinition and is useful when you already have a value or want to share a definition across multiple builders:
latDef := datasets.DataDefinition{
Name: "PLANE LATITUDE", Unit: "degrees",
Type: types.SIMCONNECT_DATATYPE_FLOAT64,
}
b.Add(latDef)
Both methods return the receiver for chaining.
Remove
Remove removes the first definition whose Name matches the argument. If no match is found, it is a no-op — it does not panic:
b.Remove("PLANE ALTITUDE") // removes if present, ignored if absent
Repeatable Build
Build is non-destructive. Each call takes an independent snapshot of the builder’s current state, so you can call Build multiple times to produce independent datasets at different stages:
b := datasets.NewBuilder().
AddField("PLANE LATITUDE", "degrees", types.SIMCONNECT_DATATYPE_FLOAT64, 0).
AddField("PLANE LONGITUDE", "degrees", types.SIMCONNECT_DATATYPE_FLOAT64, 0)
posOnly := b.Build() // [PLANE LATITUDE, PLANE LONGITUDE]
b.AddField("PLANE ALTITUDE", "feet", types.SIMCONNECT_DATATYPE_FLOAT64, 0)
withAlt := b.Build() // [PLANE LATITUDE, PLANE LONGITUDE, PLANE ALTITUDE]
// posOnly is unchanged — it still has only two fields
Aliasing safety
Build uses append([]DataDefinition{}, b.definitions...) internally, which always allocates a new backing array. The returned DataSet never shares memory with the builder or any previously built DataSet, so caller mutations are isolated.
Reset
Reset removes all definitions from the builder and returns the receiver for chaining. Use it to reuse a builder across multiple construction cycles without allocating a new one:
b := datasets.NewBuilder()
b.AddField("PLANE LATITUDE", "degrees", types.SIMCONNECT_DATATYPE_FLOAT64, 0)
snapshot1 := b.Build()
b.Reset().
AddField("AIRSPEED INDICATED", "knots", types.SIMCONNECT_DATATYPE_FLOAT64, 0)
snapshot2 := b.Build()
// snapshot1 and snapshot2 are independent; snapshot1 is unaffected by Reset
Len
Len returns the number of definitions currently held in the builder. It is useful for guard checks before calling Build:
if b.Len() == 0 {
return fmt.Errorf("dataset builder is empty")
}
ds := b.Build()
Registry
The global registry allows sub-packages to advertise named datasets via init() side-effects. Callers discover available datasets at runtime through the registry API.
Naming convention
Dataset names follow the "<category>/<descriptor>" format, for example "traffic/aircraft". The category groups related datasets and is used by ListByCategory. The name uniquely identifies a dataset across the entire process.
Registration via import side-effect
Sub-packages call datasets.Register in their init() function. Importing the sub-package triggers init(), which registers the dataset. No explicit registration call is needed in application code:
import (
"github.com/mrlm-net/simconnect/pkg/datasets"
_ "github.com/mrlm-net/simconnect/pkg/datasets/traffic" // side-effect: registers "traffic/aircraft"
)
The blank import _ is the Go idiom for importing a package solely for its init() side-effects.
Get
Get retrieves the constructor function for a named dataset. The constructor returns a fresh *DataSet on every call:
ctor, ok := datasets.Get("traffic/aircraft")
if !ok {
panic("traffic/aircraft not registered")
}
ds := ctor() // fresh *DataSet
Always check ok before calling the constructor. A missing name returns nil, false.
List and Categories
List returns the names of all registered datasets sorted alphabetically. Categories returns the distinct category names sorted alphabetically. Both are useful for introspection and tooling:
fmt.Println("Registered datasets:", datasets.List())
fmt.Println("Categories:", datasets.Categories())
ListByCategory
ListByCategory narrows the listing to a single category. Returns nil if no datasets are registered under that category:
trafficDatasets := datasets.ListByCategory("traffic")
for _, name := range trafficDatasets {
fmt.Println(name)
}
Concurrent safety
All registry operations (Register, Get, List, Categories, ListByCategory) are safe for concurrent use. The registry uses a sync.RWMutex internally: reads (Get, List, Categories, ListByCategory) acquire a read lock; writes (Register) acquire an exclusive write lock.
Panics on empty name or category
Register panics if either name or category is an empty string. This is an intentional fail-fast guard against misconfigured init() calls that would otherwise silently produce unqueryable entries:
// panics: name must not be empty
datasets.Register("", "traffic", constructor)
// panics: category must not be empty
datasets.Register("traffic/aircraft", "", constructor)
Overwrite behaviour
Registering the same name twice silently overwrites the previous entry. This allows test code and alternative implementations to replace a built-in dataset without ceremony.
End-to-end example
The following snippet ties together all four APIs: import side-effect, registry discovery, Clone, Builder, and Merge.
//go:build windows
package main
import (
"fmt"
"github.com/mrlm-net/simconnect/pkg/datasets"
_ "github.com/mrlm-net/simconnect/pkg/datasets/traffic" // registers "traffic/aircraft"
"github.com/mrlm-net/simconnect/pkg/types"
)
func main() {
// 1. Discover what is registered.
fmt.Println("Registered datasets:", datasets.List())
fmt.Println("Categories:", datasets.Categories())
// 2. Retrieve the traffic/aircraft constructor from the registry.
ctor, ok := datasets.Get("traffic/aircraft")
if !ok {
panic("traffic/aircraft not registered")
}
trafficDS := ctor() // fresh *DataSet from the constructor
// 3. Clone to get an independent copy before extending.
extended := trafficDS.Clone()
// 4. Build a minimal position dataset using the fluent builder.
posDS := datasets.NewBuilder().
AddField("PLANE LATITUDE", "degrees", types.SIMCONNECT_DATATYPE_FLOAT64, 0).
AddField("PLANE LONGITUDE", "degrees", types.SIMCONNECT_DATATYPE_FLOAT64, 0).
AddField("PLANE ALTITUDE", "feet", types.SIMCONNECT_DATATYPE_FLOAT64, 0).
Build()
// 5. Merge the cloned traffic dataset with the position dataset.
// Duplicate names (PLANE LATITUDE, PLANE LONGITUDE, PLANE ALTITUDE)
// are deduplicated with last-wins: the posDS versions replace the
// traffic dataset's versions, shifting to the end.
merged := datasets.Merge(extended, posDS)
fmt.Printf("Traffic fields: %d\n", len(trafficDS.Definitions))
fmt.Printf("Position fields: %d\n", len(posDS.Definitions))
fmt.Printf("Merged fields: %d\n", len(merged.Definitions))
}
See Also
- Engine/Client Usage —
RegisterDatasetfor submitting a dataset to SimConnect - Manager Usage — dataset registration in the managed connection lifecycle