mapper

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 12, 2025 License: MIT Imports: 3 Imported by: 0

README ΒΆ

Go AutoMapper

⚠️ Notice: This library is under active development. The API may change until version 1.0.0 is released.

a high-performance, type-safe library for mapping between Go types using generics. It offers flexible mapping strategies, including manual, function-based, and automatic mapping, with a focus on speed and zero allocations for mapped functions. The library is easy to integrate, supports bidirectional mappings, and provides comprehensive documentation and benchmarks to guide usage and performance optimization.

πŸš€ Features

  • Type-Safe: Uses Go generics for compile-time type safety
  • High Performance: ~50ns per mapping operation with zero allocations
  • Flexible: Support for any type conversion (primitives, structs, pointers, interfaces)
  • Simple API: Clean and intuitive interface
  • Comprehensive Testing: 100% test coverage with extensive benchmarks

πŸ›  Installation

go get github.com/hotrungnhan/go-automapper

πŸ“– Quick Start

There are 3 ways to map data between types in Go:

1. Manual (No Library)

type Person struct {
    Name string
    Age  int
}

// Manual conversion between types
func PersonToDTO(p Person) PersonDTO {
    return PersonDTO{
        FullName: p.Name,
        Years:    p.Age,
    }
}

person := Person{Name: "Alice", Age: 28}
dto := PersonToDTO(person)

Pros: Fastest possible, zero overhead. Cons: Hard to manage, not generic, lots of boilerplate.


2. Direct Mapping
import "github.com/hotrungnhan/go-automapper"

func main(){
    m := mapper.NewMapper()

    // Single Direction - Direct mapping
    mapper.Register(m, func(p Person) PersonDTO {
        return PersonDTO{
            FullName: p.Name,
            Years:    p.Age,
        }
    })

    person := Person{Name: "Alice", Age: 28}
    dto, err := mapper.Map[Person, PersonDTO](m, person)
    if err != nil {
        panic(err)
    }
}

Pros: Type-safe, maintainable, generic, and almost as fast as manual mapping. Cons: Significan Slower(50x), You still write mapping functions, but only once per type pair.

3. AutoMap

import "github.com/hotrungnhan/go-automapper"

func main(){
    m := mapper.NewMapper()

    // bidirection
    mapper.RegisterAutoMap[Person, PersonDTO](m)
    person := Person{Name: "Alice", Age: 28}
    dto, err := mapper.Map[Person, PersonDTO](m, person)
    if err != nil {
        panic(err)
    }
}


Pros: No mapping function needed, works for most structs with similar fields.
Cons: Dramatically slower (500x+), allocates memory, not for hot paths.

πŸ”§ Advanced Usage

Bidirectional Mappings
// Register both directions
mapper.Register(m, func(p Person) PersonDTO {
    return PersonDTO{FullName: p.Name, Years: p.Age}
})

mapper.Register(m, func(dto PersonDTO) Person {
    return Person{Name: dto.FullName, Age: dto.Years}
})
Registry Management
// List all mappings for debugging
mappings := mapper.List(m)
for _, mapping := range mappings {
    fmt.Println("Registered:", mapping)
}

// Clean up mappings
mapper.Remove[OldSource, OldDest](m)**

⚑ Performance Tips

  1. Reuse Mapper Instances: Create one mapper per application lifecycle
  2. Register Once: Register mappings at startup, not per operation
  3. Avoid Complex Logic: Keep mapping functions simple and fast
  4. Batch Operations: Process collections when possible
// Good: Simple, fast mapping
mapper.Register(m, func(s string) int { return len(s) })

// Avoid: Complex operations in mapping
mapper.Register(m, func(url string) Data {
    // Don't do HTTP calls or heavy computation here
    return fetchDataFromAPI(url) // ❌
})
🏎️ Benchmark
Benchmark Ops/sec ns/op Baseline (x)
Struct Mapping
Manual 1,000,000,000 0.42 1x
DirectMap 3,636,888 331.9 790x
AutoMap 610,694 1,999 4,760x
Slice Mapping
ManualSliceMap 370,471 3,367 1x
DirectMapSlice 3,912 346,897 103x
AutoMapSlice 726 1,682,741 500x

Lower ns/op is better. Manual mapping is fastest, followed by direct mapping, then automap.

πŸ“ˆ Recommendation by Use Case
  • Manual Mapping:

    • Best for: Ultra-high-performance, hot code paths, or extremely simple mappings.
    • Why: Manual code is always fastest (sub-nanosecond), with zero overhead.
  • With Mapped Function:

    • Best for: Most application code, especially when you want type safety, maintainability, and flexibility.
    • Why: Only ~25ns/op, zero allocations, and much easier to maintain than manual mapping for many types.
  • With Automapper:

    • Best for: When you need advanced features not present here, and performance is less critical.
    • Why: Typically 50–60x slower (1500+ ns/op) and introduce allocations.

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (go test ./...)
  5. Run benchmarks (go test -bench=.)
  6. Commit your changes (git commit -am 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request
πŸ“ Semantic Commit Emoji Guide

Use the following semantic emoji in your commit messages to indicate the type of change:

Type Emoji Code Description
feat ✨ :sparkles: A new feature
fix πŸ› :bug: A bug fix
docs πŸ“š :books: Documentation only changes
style πŸ’Ž :gem: Code style changes (formatting, etc.)
refactor πŸ”¨ :hammer: Code refactoring (no feature or fix)
perf πŸš€ :rocket: Performance improvements
test 🚨 :rotating_light: Adding or updating tests
build πŸ“¦ :package: Build system or external dependency changes
ci πŸ‘· :construction_worker: CI/CD configuration changes
chore πŸ”§ :wrench: Other changes that don't modify src or tests

Example commit message:

✨ feat: Add support for custom mapping functions

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸŽ– Acknowledgments

  • Inspired by AutoMapper (.NET) and similar mapping libraries
  • Built with Go's powerful generics and reflection capabilities
  • Optimized for high-performance applications

Made with ❀️ for the Go community

Documentation ΒΆ

Overview ΒΆ

Package mapper provides a type-safe, generic mapping library for Go that allows registration and execution of mapping functions between different types. It supports both manual mapping function registration and automatic field mapping.

Package mapper provides automatic mapping functionality using reflection and the jinzhu/copier library

Index ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

View Source
var ErrNoMapping = errors.New("no mapping function registered for this type pair")

ErrNoMapping is returned when attempting to map between types that don't have a registered mapping function.

View Source
var ErrSrcAndDestMustBeSlices = errors.New("both source and destination must be slices")

ErrSrcAndDestMustBeSlices is returned when a function expects both the source and destination parameters to be slices, but one or both are not. This error helps enforce type safety when performing operations that require slice types.

View Source
var Global = New()

Global is a default mapper instance that can be used for convenience. It provides a shared mapping registry for applications that don't need multiple mapper instances.

Functions ΒΆ

func Has ΒΆ added in v0.2.0

func Has[S any, D any](m Mapper) bool

Has checks if a mapping function is registered for the specified type pair. This is useful for conditional mapping or validation before attempting to map.

Type Parameters:

  • S: Source type to check for mapping
  • D: Destination type to check for mapping

Parameters:

  • m: The mapper instance to check

Returns:

  • bool: true if a mapping function is registered for S -> D, false otherwise

Example:

mapper := New()
Register(mapper, func(s string) int { return len(s) })

if Has[string, int](mapper) {
    result, _ := Map[string, int](mapper, "hello")
    fmt.Println("Mapped:", result)
} else {
    fmt.Println("No mapping available")
}

fmt.Println(Has[int, string](mapper)) // Output: false

func List ΒΆ

func List(m Mapper) []string

List returns a slice of strings representing all registered mapping type pairs. Each string is formatted as "SourceType-DestinationType" and can be used for debugging, logging, or displaying available mappings to users.

Parameters:

  • m: The mapper instance to list mappings from

Returns:

  • []string: A slice of strings representing all registered type pair mappings

Example:

mapper := New()
Register(mapper, func(s string) int { return len(s) })
Register(mapper, func(i int) string { return fmt.Sprintf("%d", i) })
Register(mapper, func(p Person) PersonDTO { return PersonDTO{Name: p.Name} })

mappings := List(mapper)
for _, mapping := range mappings {
    fmt.Println("Available mapping:", mapping)
}
// Output:
// Available mapping: string-int
// Available mapping: int-string
// Available mapping: main.Person-main.PersonDTO

func Map ΒΆ

func Map[S any, D any](m Mapper, src S) (D, error)

Map executes a registered mapping function to convert a value from type S to type D. It supports mapping between values, pointers, and mixed value/pointer combinations. The function automatically handles pointer dereferencing and creation as needed.

Type Parameters:

  • S: Source type (must match a registered mapping function's input type)
  • D: Destination type (must match a registered mapping function's output type)

Parameters:

  • m: The mapper instance containing the registered mapping functions
  • src: The source value to be mapped

Returns:

  • D: The mapped result of type D
  • error: ErrNoMapping if no mapping function is registered for the type pair

Supported mapping combinations:

  • Value to Value: T -> U
  • Pointer to Pointer: *T -> *U (nil input returns nil output)
  • Value to Pointer: T -> *U
  • Pointer to Value: *T -> U (nil input returns zero value)

Example:

mapper := New()
Register(mapper, func(s string) int { return len(s) })

result, err := Map[string, int](mapper, "hello")
if err != nil {
    log.Fatal(err)
}
fmt.Println(result) // Output: 5

// Pointer mapping
name := "world"
ptrResult, err := Map[*string, *int](mapper, &name)
if err != nil {
    log.Fatal(err)
}
fmt.Println(*ptrResult) // Output: 5

func MapSlice ΒΆ added in v0.2.0

func MapSlice[S any, D any](m Mapper, src S) (D, error)

MapSlice applies a registered mapping function to each element of a slice, returning a new slice with the mapped elements. It supports various combinations of slice element types including values and pointers.

Type Parameters:

  • S: Source slice type (e.g., []SourceType, []*SourceType)
  • D: Destination slice type (e.g., []DestType, []*DestType)

Parameters:

  • m: The mapper instance containing the registered mapping functions
  • src: The source slice to be mapped

Returns:

  • D: A new slice containing the mapped elements
  • error: ErrNoMapping if no mapping function is registered for the element types, or an error if source/destination are not slices

Supported slice mapping combinations:

  • []T -> []U: Value elements to value elements
  • []*T -> []*U: Pointer elements to pointer elements (nil elements remain nil)
  • []T -> []*U: Value elements to pointer elements
  • []*T -> []U: Pointer elements to value elements (nil elements become zero values)

Example:

mapper := New()
Register(mapper, func(s string) int { return len(s) })

words := []string{"hello", "world", "go"}
lengths, err := MapSlice[[]string, []int](mapper, words)
if err != nil {
    log.Fatal(err)
}
fmt.Println(lengths) // Output: [5 5 2]

// Pointer slice mapping
wordPtrs := []*string{&words[0], nil, &words[2]}
lengthPtrs, err := MapSlice[[]*string, []*int](mapper, wordPtrs)
if err != nil {
    log.Fatal(err)
}
// lengthPtrs will be [*5, nil, *2]

func MustMap ΒΆ added in v0.3.0

func MustMap[S any, D any](m Mapper, src S) D

MustMap is like Map but panics if mapping fails. It is useful for cases where mapping failure is considered a programmer error.

Type Parameters:

  • S: Source type
  • D: Destination type

Parameters:

  • m: The mapper instance
  • src: The source value to be mapped

Returns:

  • D: The mapped result of type D

Panics:

  • If no mapping function is registered for the type pair, or if mapping fails

Example:

mapper := New()
Register(mapper, func(s string) int { return len(s) })
result := MustMap[string, int](mapper, "hello")
fmt.Println(result) // Output: 5

func MustMapSlice ΒΆ added in v0.3.0

func MustMapSlice[S any, D any](m Mapper, src S) D

MustMapSlice is like MapSlice but panics if mapping fails. It is useful for cases where mapping failure is considered a programmer error.

Type Parameters:

  • S: Source slice type (e.g., []SourceType, []*SourceType)
  • D: Destination slice type (e.g., []DestType, []*DestType)

Parameters:

  • m: The mapper instance
  • src: The source slice to be mapped

Returns:

  • D: The mapped result slice of type D

Panics:

  • If no mapping function is registered for the element types, or if mapping fails

Example:

mapper := New()
Register(mapper, func(s string) int { return len(s) })
result := MustMapSlice[[]string, []int](mapper, []string{"a", "bb"})
fmt.Println(result) // Output: [1 2]

func Register ΒΆ

func Register[S any, D any](m Mapper, fn func(S) D)

Register registers a mapping function for converting from type S to type D. The function will be stored in the mapper's registry and can be used by Map and MapSlice. If a mapping for the same type pair already exists, it will be overwritten.

Type Parameters:

  • S: Source type (input type for the mapping function)
  • D: Destination type (output type for the mapping function)

Parameters:

  • m: The mapper instance to register the function with
  • fn: The mapping function that converts from S to D

Example:

mapper := New()
Register(mapper, func(s string) int { return len(s) })
Register(mapper, func(p Person) PersonDTO {
    return PersonDTO{Name: p.FirstName + " " + p.LastName}
})

func RegisterAutoMap ΒΆ

func RegisterAutoMap[S any, D any](m Mapper)

RegisterAutoMap registers bidirectional automatic mapping functions for types S and D. This function creates mapping functions that use reflection to automatically copy matching fields between structs. Both S->D and D->S mappings are registered.

The automatic mapping uses the jinzhu/copier library, which copies fields with matching names and compatible types. This is convenient for mapping between similar structs but comes with a performance cost compared to manually registered functions.

Performance Note: AutoMap functions are approximately 50x slower than manually registered mapping functions (~1600ns vs ~25ns per operation) due to reflection overhead.

Type Parameters:

  • S: Source type for bidirectional mapping
  • D: Destination type for bidirectional mapping

Parameters:

  • m: The mapper instance to register the automatic mapping functions with

Example:

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Email string `json:"email"`
}

type UserDTO struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Email string `json:"email"`
}

mapper := New()
RegisterAutoMap[User, UserDTO](mapper)

user := User{ID: 1, Name: "John", Email: "john@example.com"}
dto, err := Map[User, UserDTO](mapper, user)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Mapped DTO: %+v\n", dto)

// Reverse mapping is also available
backToUser, err := Map[UserDTO, User](mapper, dto)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Mapped back: %+v\n", backToUser)

func Remove ΒΆ added in v0.2.0

func Remove[S any, D any](m Mapper)

Remove unregisters a mapping function for the specified type pair. After removal, attempting to map between these types will return ErrNoMapping. This operation is safe to call even if no mapping exists for the type pair.

Type Parameters:

  • S: Source type to remove mapping for
  • D: Destination type to remove mapping for

Parameters:

  • m: The mapper instance to remove the mapping from

Example:

mapper := New()
Register(mapper, func(s string) int { return len(s) })

fmt.Println(Has[string, int](mapper)) // Output: true

Remove[string, int](mapper)
fmt.Println(Has[string, int](mapper)) // Output: false

_, err := Map[string, int](mapper, "hello")
fmt.Println(err) // Output: no mapping function registered for this type pair

Types ΒΆ

type Mapper ΒΆ

type Mapper struct {
	// contains filtered or unexported fields
}

Mapper is the main mapping registry that stores mapping functions between type pairs. Each mapper instance maintains its own independent registry of mapping functions.

func New ΒΆ added in v0.2.0

func New() Mapper

New creates a new Mapper instance with an empty registry. Each mapper maintains its own independent registry of mapping functions.

Returns:

  • Mapper: A new mapper instance with an initialized empty registry

Example:

mapper := New()
Register(mapper, func(s string) int { return len(s) })
result, err := Map[string, int](mapper, "hello")

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL