A lightweight, production-ready Go package for pointer operations with zero dependencies and full generic type support.
This package solves common pointer-related challenges in Go:
- Creating pointers from literals and constants
- Safe dereferencing without nil panics
- Optional field handling in APIs and configuration
- Batch pointer operations on slices and maps
- Type-safe conversions for all Go built-in types
- Functional programming operations (map, filter, flatMap) for pointer values
Key Features:
- Generic functions supporting any type (Go 1.18+)
- Type-specific helpers for better IDE autocomplete
- Zero dependencies, pure Go standard library
- Sub-nanosecond performance with zero allocations
- Comprehensive test coverage and production-proven
- Installation
- Quick Start
- Core Concepts
- API Documentation
- Practical Examples
- API Reference
- Performance
- Best Practices
- FAQ
- Contributing
- License
go get go.companyinfo.dev/ptrRequirements:
- Go 1.18 or later (for generics support)
- No external dependencies
package main
import (
"fmt"
"go.companyinfo.dev/ptr"
)
func main() {
// Problem: Can't take address of literals
// var name *string = &"Alice" // ❌ Compile error
// Solution: Use ptr.String()
name := ptr.String("Alice") // ✅ Works
// Problem: Nil dereference causes panic
var nilPtr *string
// fmt.Println(*nilPtr) // ❌ Panic!
// Solution: Safe dereferencing
fmt.Println(ptr.ToString(nilPtr)) // ✅ Returns "" safely
// Generic support for any type
type User struct { Name string }
user := ptr.To(User{Name: "Bob"})
fmt.Println(ptr.From(user).Name) // "Bob"
}Pointers are essential in Go for:
- Optional fields: Distinguishing between zero values and missing values
- API design: Making struct fields optional (e.g.,
omitemptyin JSON) - Database nullability: Representing NULL values from SQL databases
- Performance: Avoiding large struct copies
Go doesn't allow direct address-taking of literals or constants:
// These don't work:
s := &"hello" // ❌ Cannot take address of "hello"
i := &42 // ❌ Cannot take address of 42
b := &true // ❌ Cannot take address of true
// Traditional workaround:
str := "hello"
s := &str // ✅ Works but verbose
// With ptr:
s := ptr.String("hello") // ✅ Clean and simple
i := ptr.Int(42)
b := ptr.Bool(true)Dereferencing nil pointers causes runtime panics. This package provides safe alternatives:
var s *string // nil
// Unsafe:
// value := *s // ❌ Panic!
// Safe with ptr:
value := ptr.From(s) // Returns "" (zero value)
value := ptr.FromOr(s, "default") // Returns "default"
value := ptr.MustFrom(s) // Panics with clear message (for programmer errors)See how ptr simplifies common pointer operations:
// Before (manual approach)
func makeStringPtr(s string) *string {
return &s
}
func makeIntPtr(i int) *int {
return &i
}
name := makeStringPtr("Alice")
age := makeIntPtr(30)
// After (with ptr)
name := ptr.String("Alice")
age := ptr.Int(30)// Before (manual with risk)
var name *string
value := ""
if name != nil {
value = *name
}
// After (with ptr)
value := ptr.ToString(name) // Returns "" if nil// Before (manual)
var timeout *int
finalTimeout := 30
if timeout != nil {
finalTimeout = *timeout
}
// After (with ptr)
finalTimeout := ptr.GetOr(timeout, 30)// Before (manual)
func pointersEqual(a, b *int) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return *a == *b
}
// After (with ptr)
equal := ptr.Equal(a, b)// Before (manual)
var age *int
var isAdult bool
if age != nil && *age >= 18 {
isAdult = true
}
// After (with ptr)
adult := ptr.Filter(age, func(a int) bool { return a >= 18 })
isAdult := adult != nil// Before (manual)
values := []int{1, 2, 3}
ptrs := make([]*int, len(values))
for i := range values {
ptrs[i] = &values[i]
}
// After (with ptr)
ptrs := ptr.IntSlice([]int{1, 2, 3})These functions work with any type using Go generics:
Create a pointer from a value:
s := ptr.To("hello") // *string
i := ptr.To(42) // *int
b := ptr.To(true) // *bool
type User struct {
Name string
Age int
}
u := ptr.To(User{Name: "Alice", Age: 30}) // *UserDereference a pointer with zero-value fallback:
s := ptr.To("hello")
fmt.Println(ptr.From(s)) // "hello"
var nilStr *string
fmt.Println(ptr.From(nilStr)) // "" (empty string)
var nilInt *int
fmt.Println(ptr.From(nilInt)) // 0Dereference a pointer with custom default value:
s := ptr.To("hello")
fmt.Println(ptr.FromOr(s, "default")) // "hello"
var nilStr *string
fmt.Println(ptr.FromOr(nilStr, "default")) // "default"Dereference a pointer and panic if nil (use only when nil is a programming error):
s := ptr.To("hello")
fmt.Println(ptr.MustFrom(s)) // "hello"
var nilStr *string
fmt.Println(ptr.MustFrom(nilStr)) // panics!Return the first non-nil pointer from a list:
var envPort *int // nil
var configPort *int // nil
defaultPort := ptr.Int(8080)
port := ptr.Coalesce(envPort, configPort, defaultPort)
fmt.Println(*port) // 8080Safely set a pointer value with nil-check:
p := ptr.To(42)
ptr.Set(p, 100) // *p is now 100, returns true
ptr.Set[int](nil, 100) // no-op, returns falseTransform a pointer value with a function:
s := ptr.To("hello")
length := ptr.Map(s, func(s string) int { return len(s) })
fmt.Println(*length) // 5
// Returns nil if input is nil
var nilStr *string
result := ptr.Map(nilStr, func(s string) int { return len(s) })
// result is nilConvert a slice of values to a slice of pointers:
ages := []int{25, 30, 35, 40}
agePointers := ptr.ToSlice(ages)
// []*int with pointers to each valueConvert a slice of pointers to a slice of values:
pointers := []*int{ptr.Int(1), nil, ptr.Int(3)}
values := ptr.FromSlice(pointers)
fmt.Println(values) // [1 0 3] - nil becomes zero valueConvert a map with value type T to a map with pointer value type *T:
config := map[string]int{
"timeout": 30,
"retries": 3,
}
configPtrs := ptr.ToMap(config)
// map[string]*int with pointer valuesConvert a map with pointer value type *T to a map with value type T:
settings := map[string]*string{
"host": ptr.String("localhost"),
"port": nil,
}
values := ptr.FromMap(settings)
fmt.Println(values) // map[host:localhost port:] - nil becomes empty stringSafely compare two pointers:
a := ptr.To(42)
b := ptr.To(42)
c := ptr.To(99)
ptr.Equal(a, b) // true (same value)
ptr.Equal(a, c) // false (different values)
ptr.Equal[int](nil, nil) // true (both nil)
ptr.Equal(a, nil) // false (one nil)Create a shallow copy of a pointer:
original := ptr.To(42)
copied := ptr.Copy(original)
*original = 100
fmt.Println(*copied) // 42 (unchanged)Check if a pointer is nil:
s := ptr.To("hello")
ptr.IsNil(s) // false
ptr.IsNil[string](nil) // trueReturn first non-nil pointer, or second if first is nil:
primary := ptr.To(42)
fallback := ptr.To(100)
ptr.Or(primary, fallback) // returns primary (42)
ptr.Or[int](nil, fallback) // returns fallback (100)Create pointer only if value is not zero:
ptr.NonZero(42) // returns *int pointing to 42
ptr.NonZero(0) // returns nil
ptr.NonZero("") // returns nil
ptr.NonZero("hi") // returns *string pointing to "hi"Check if pointer is nil or points to zero value:
ptr.IsZero(ptr.To(0)) // true
ptr.IsZero(ptr.To(42)) // false
ptr.IsZero[int](nil) // trueReturn pointer if predicate is true, otherwise nil:
age := ptr.To(25)
valid := ptr.Filter(age, func(a int) bool { return a >= 18 })
// valid points to 25
young := ptr.To(15)
invalid := ptr.Filter(young, func(a int) bool { return a >= 18 })
// invalid is nilApply transformation that returns a pointer:
s := ptr.To("42")
result := ptr.FlatMap(s, func(str string) *int {
val, err := strconv.Atoi(str)
if err == nil {
return ptr.To(val)
}
return nil
})
// result points to 42Execute function if pointer is not nil:
debug := ptr.To(true)
executed := ptr.Apply(debug, func(b bool) {
fmt.Println("Debug mode:", b)
})
// prints "Debug mode: true", returns true
var nilPtr *bool
executed = ptr.Apply(nilPtr, func(b bool) {
fmt.Println("Won't print")
})
// returns false, function not executedTransform pointer value in-place:
price := ptr.To(100.0)
ptr.Modify(price, func(p float64) float64 {
return p * 0.8 // 20% discount
})
// *price is now 80.0Exchange values of two pointers:
a := ptr.To(1)
b := ptr.To(2)
ptr.Swap(a, b)
// *a is now 2, *b is now 1For better IDE autocomplete and convenience, the package provides type-specific functions:
name := ptr.String("Alice")
fmt.Println(ptr.ToString(name)) // "Alice"
fmt.Println(ptr.ToString(nil)) // ""age := ptr.Int(30)
fmt.Println(ptr.ToInt(age)) // 30
fmt.Println(ptr.ToInt(nil)) // 0id := ptr.Int64(123456789)
fmt.Println(ptr.ToInt64(id)) // 123456789
fmt.Println(ptr.ToInt64(nil)) // 0active := ptr.Bool(true)
fmt.Println(ptr.ToBool(active)) // true
fmt.Println(ptr.ToBool(nil)) // falseprice := ptr.Float64(19.99)
fmt.Println(ptr.ToFloat64(price)) // 19.99
fmt.Println(ptr.ToFloat64(nil)) // 0.0The package also provides helpers for all Go numeric types:
// Integer types
i8 := ptr.Int8(127)
i16 := ptr.Int16(32767)
i32 := ptr.Int32(2147483647)
// Unsigned integer types
u := ptr.Uint(42)
u8 := ptr.Uint8(255)
u16 := ptr.Uint16(65535)
u32 := ptr.Uint32(4294967295)
u64 := ptr.Uint64(18446744073709551615)
// Float types
f32 := ptr.Float32(3.14)
// Character types
b := ptr.Byte('A')
r := ptr.Rune('世')
// Pointer type
uptr := ptr.Uintptr(0x1234)Helpers for working with time-related types:
// Time helpers
now := ptr.Time(time.Now())
timestamp := ptr.ToTime(now) // time.Time value
fmt.Println(ptr.ToTime(nil).IsZero()) // true
// Duration helpers
timeout := ptr.Duration(30 * time.Second)
d := ptr.ToDuration(timeout) // time.Duration value
fmt.Println(ptr.ToDuration(nil)) // 0Helpers for complex number types:
// Complex64
c64 := ptr.Complex64(3 + 4i)
val64 := ptr.ToComplex64(c64) // complex64 value
// Complex128
c128 := ptr.Complex128(3 + 4i)
val128 := ptr.ToComplex128(c128) // complex128 valueFor better IDE autocomplete and convenience, type-specific slice conversion functions are available for all common types:
names := []string{"Alice", "Bob", "Charlie"}
namePtrs := ptr.StringSlice(names)
// []*string with pointers to each name
mixed := []*string{ptr.String("Alice"), nil, ptr.String("Charlie")}
values := ptr.ToStringSlice(mixed)
fmt.Println(values) // [Alice Charlie] - nil becomes empty string// Works with all integer types: Int, Int8, Int16, Int32, Int64
ages := []int{25, 30, 35}
agePtrs := ptr.IntSlice(ages)
ids := []int64{1001, 1002, 1003}
idPtrs := ptr.Int64Slice(ids)
// Convert back to values
values := ptr.ToIntSlice(agePtrs) // [25 30 35]// Unsigned integers: Uint, Uint8, Uint16, Uint32, Uint64
counts := []uint{1, 2, 3}
countPtrs := ptr.UintSlice(counts)
// Floats: Float32, Float64
prices := []float64{19.99, 29.99, 39.99}
pricePtrs := ptr.Float64Slice(prices)
// Bytes
data := []byte{0x01, 0x02, 0x03}
dataPtrs := ptr.ByteSlice(data)flags := []bool{true, false, true}
flagPtrs := ptr.BoolSlice(flags)
values := ptr.ToBoolSlice(flagPtrs) // [true false true]// Time slices
timestamps := []time.Time{time.Now(), time.Now().Add(time.Hour)}
timePtrs := ptr.TimeSlice(timestamps)
// Duration slices
durations := []time.Duration{time.Second, time.Minute, time.Hour}
durationPtrs := ptr.DurationSlice(durations)Available slice functions for all types:
IntSlice,Int8Slice,Int16Slice,Int32Slice,Int64SliceUintSlice,Uint8Slice,Uint16Slice,Uint32Slice,Uint64SliceFloat32Slice,Float64SliceBoolSlice,ByteSlice,StringSliceTimeSlice,DurationSlice
And their corresponding To*Slice functions for converting back to values.
Type-specific map conversion functions for converting between map[string]T and map[string]*T:
settings := map[string]string{
"host": "localhost",
"port": "8080",
}
settingPtrs := ptr.StringMap(settings)
// map[string]*string with pointer values
values := ptr.ToStringMap(settingPtrs)
// Back to map[string]string// Works with all integer types: Int, Int8, Int16, Int32, Int64
config := map[string]int{
"timeout": 30,
"retries": 3,
"maxConns": 100,
}
configPtrs := ptr.IntMap(config)
// With nil values
mixed := map[string]*int{
"timeout": ptr.Int(30),
"retries": nil, // Will become 0
}
values := ptr.ToIntMap(mixed) // map[string]int// Unsigned integers: Uint, Uint8, Uint16, Uint32, Uint64
limits := map[string]uint64{
"maxSize": 1024000,
"maxFiles": 100,
}
limitPtrs := ptr.Uint64Map(limits)
// Floats: Float32, Float64
prices := map[string]float64{
"basic": 9.99,
"premium": 19.99,
"enterprise": 99.99,
}
pricePtrs := ptr.Float64Map(prices)features := map[string]bool{
"caching": true,
"monitoring": true,
"debug": false,
}
featurePtrs := ptr.BoolMap(features)
values := ptr.ToBoolMap(featurePtrs)// Time maps
events := map[string]time.Time{
"created": time.Now(),
"updated": time.Now(),
}
eventPtrs := ptr.TimeMap(events)
// Duration maps
timeouts := map[string]time.Duration{
"read": 30 * time.Second,
"write": 10 * time.Second,
}
timeoutPtrs := ptr.DurationMap(timeouts)Available map functions for all types:
IntMap,Int8Map,Int16Map,Int32Map,Int64MapUintMap,Uint8Map,Uint16Map,Uint32Map,Uint64MapFloat32Map,Float64MapBoolMap,ByteMap,StringMapTimeMap,DurationMap
And their corresponding To*Map functions for converting back to value maps.
Building APIs with optional query parameters and request bodies:
// Request model with optional filters
type ListUsersRequest struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Role *string `json:"role,omitempty"` // Optional filter
Active *bool `json:"active,omitempty"` // Optional filter
MinAge *int `json:"min_age,omitempty"` // Optional filter
}
// Handler function
func ListUsers(req ListUsersRequest) ([]User, error) {
query := "SELECT * FROM users WHERE 1=1"
args := []interface{}{}
// Build dynamic query based on optional fields
if !ptr.IsNil(req.Role) {
query += " AND role = ?"
args = append(args, ptr.ToString(req.Role))
}
if !ptr.IsNil(req.Active) {
query += " AND active = ?"
args = append(args, ptr.ToBool(req.Active))
}
if !ptr.IsNil(req.MinAge) {
query += " AND age >= ?"
args = append(args, ptr.ToInt(req.MinAge))
}
// Execute query...
return fetchUsers(query, args...)
}
// Client usage
req := ListUsersRequest{
Page: 1,
PageSize: 50,
Role: ptr.String("admin"), // Filter by role
Active: ptr.Bool(true), // Filter by active status
MinAge: nil, // Don't filter by age
}type User struct {
Name string `json:"name"`
Email *string `json:"email,omitempty"`
Age *int `json:"age,omitempty"`
}
user := User{
Name: "Alice",
Email: ptr.String("[email protected]"),
Age: ptr.Int(30),
}
// Email and Age will be included in JSON
// If set to nil, they will be omittedtype APIResponse struct {
Data *UserData
Error *string
}
func processResponse(resp APIResponse) {
if !ptr.IsNil(resp.Error) {
log.Printf("Error: %s", ptr.ToString(resp.Error))
return
}
// Safely access data with fallback
name := ptr.FromOr(resp.Data.Name, "Unknown")
fmt.Printf("User: %s\n", name)
}func CreateUser(name string, age *int, email *string) User {
return User{
Name: name,
Age: ptr.ToInt(age), // 0 if nil
Email: ptr.ToString(email), // "" if nil
}
}
// Call with optional parameters
user1 := CreateUser("Alice", ptr.Int(30), ptr.String("[email protected]"))
user2 := CreateUser("Bob", nil, nil)type Product struct {
ID int
Name string
Description *string // NULL in database if nil
Price *float64 // NULL in database if nil
}
func getProduct(id int) Product {
// ... fetch from database
return Product{
ID: id,
Name: "Product Name",
Description: ptr.String("A great product"),
Price: ptr.Float64(29.99),
}
}Real-world configuration loading with multiple sources:
type AppConfig struct {
Host string
Port int
Timeout time.Duration
MaxRetries int
Debug bool
}
// LoadConfig demonstrates precedence: env vars > config file > defaults
func LoadConfig() AppConfig {
// Try to load from environment
var envPort *int
if portStr := os.Getenv("APP_PORT"); portStr != "" {
if port, err := strconv.Atoi(portStr); err == nil {
envPort = ptr.Int(port)
}
}
// Try to load from config file
var filePort *int
if config := loadConfigFile(); config != nil {
filePort = config.Port
}
// Define defaults
defaultPort := ptr.Int(8080)
defaultTimeout := ptr.Duration(30 * time.Second)
defaultRetries := ptr.Int(3)
defaultDebug := ptr.Bool(false)
// Use Coalesce for precedence: env > file > default
return AppConfig{
Host: getEnvOr("APP_HOST", "localhost"),
Port: ptr.ToInt(ptr.Coalesce(envPort, filePort, defaultPort)),
Timeout: ptr.ToDuration(ptr.Coalesce(getEnvDuration("APP_TIMEOUT"), defaultTimeout)),
MaxRetries: ptr.ToInt(ptr.Coalesce(getEnvInt("MAX_RETRIES"), defaultRetries)),
Debug: ptr.ToBool(ptr.Coalesce(getEnvBool("DEBUG"), defaultDebug)),
}
}
// Helper to get env var with default
func getEnvOr(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}Handling partial updates where only provided fields should be updated:
type UpdateUserRequest struct {
Name *string `json:"name,omitempty"`
Email *string `json:"email,omitempty"`
Age *int `json:"age,omitempty"`
Active *bool `json:"active,omitempty"`
}
func (s *UserService) UpdateUser(id int, req UpdateUserRequest) error {
// Only update fields that were explicitly provided
updates := make(map[string]interface{})
if !ptr.IsNil(req.Name) {
updates["name"] = ptr.ToString(req.Name)
}
if !ptr.IsNil(req.Email) {
updates["email"] = ptr.ToString(req.Email)
}
if !ptr.IsNil(req.Age) {
updates["age"] = ptr.ToInt(req.Age)
}
if !ptr.IsNil(req.Active) {
updates["active"] = ptr.ToBool(req.Active)
}
if len(updates) == 0 {
return errors.New("no fields to update")
}
return s.db.UpdateUser(id, updates)
}
// Usage:
// PATCH /users/123 with {"email": "[email protected]"}
// Only email is updated, other fields remain unchanged
req := UpdateUserRequest{
Email: ptr.String("[email protected]"),
// Name, Age, Active are nil - won't be updated
}// Convert user IDs to pointers for JSON
userIDs := []int64{1001, 1002, 1003}
idPointers := ptr.Int64Slice(userIDs)
// Process results
results := []*ProcessResult{
processUser(idPointers[0]),
processUser(idPointers[1]),
processUser(idPointers[2]),
}
// Extract values (nil results become zero values)
resultValues := ptr.FromSlice(results)Working with bulk data using type-specific helpers:
// Convert multiple prices at once
priceList := []float64{9.99, 19.99, 29.99, 39.99}
pricePtrs := ptr.Float64Slice(priceList)
// Use in API response
type ProductList struct {
Prices []*float64 `json:"prices,omitempty"`
}
// Convert configuration maps
envVars := map[string]string{
"DATABASE_HOST": "localhost",
"DATABASE_PORT": "5432",
"API_KEY": "secret",
}
envVarPtrs := ptr.StringMap(envVars)
// Process and convert back
processedVars := processConfig(envVarPtrs)
finalConfig := ptr.ToStringMap(processedVars)// Transform API response data
type APIUser struct {
ID int64
Name string
Roles []string
}
// Convert roles to pointers for optional field handling
users := []APIUser{
{ID: 1, Name: "Alice", Roles: []string{"admin", "user"}},
{ID: 2, Name: "Bob", Roles: []string{"user"}},
}
// Extract IDs as pointers
ids := make([]int64, len(users))
for i, u := range users {
ids[i] = u.ID
}
idPtrs := ptr.Int64Slice(ids)
// Use in bulk operations
results := bulkFetchUserData(idPtrs)// Transform pointer values without nil checks
names := []*string{
ptr.String("alice"),
ptr.String("bob"),
nil,
}
// Map to uppercase
for i, name := range names {
names[i] = ptr.Map(name, strings.ToUpper)
}
// Result: ["ALICE", "BOB", nil]| Function | Description |
|---|---|
To[T any](v T) *T |
Create a pointer from a value |
From[T any](p *T) T |
Dereference with zero-value fallback |
FromOr[T any](p *T, defaultValue T) T |
Dereference with custom default |
MustFrom[T any](p *T) T |
Dereference and panic if nil |
Coalesce[T any](ptrs ...*T) *T |
Return first non-nil pointer from list |
Set[T any](p *T, value T) bool |
Set pointer value with nil-check |
Map[T, R any](p *T, fn func(T) R) *R |
Transform pointer value with function |
Equal[T comparable](a, b *T) bool |
Compare two pointers safely |
Copy[T any](p *T) *T |
Create a shallow copy of a pointer |
IsNil[T any](p *T) bool |
Check if pointer is nil |
| Function | Description |
|---|---|
ToSlice[T any](values []T) []*T |
Convert slice of values to slice of pointers |
FromSlice[T any](ptrs []*T) []T |
Convert slice of pointers to slice of values |
| Function | Description |
|---|---|
ToMap[T any](values map[string]T) map[string]*T |
Convert map of values to map of pointer values |
FromMap[T any](ptrs map[string]*T) map[string]T |
Convert map of pointer values to map of values |
| Type | Create | Dereference |
|---|---|---|
| string | String(v string) *string |
ToString(p *string) string |
| int | Int(v int) *int |
ToInt(p *int) int |
| int64 | Int64(v int64) *int64 |
ToInt64(p *int64) int64 |
| bool | Bool(v bool) *bool |
ToBool(p *bool) bool |
| float64 | Float64(v float64) *float64 |
ToFloat64(p *float64) float64 |
| Type | Create | Dereference |
|---|---|---|
| int8 | Int8(v int8) *int8 |
ToInt8(p *int8) int8 |
| int16 | Int16(v int16) *int16 |
ToInt16(p *int16) int16 |
| int32 | Int32(v int32) *int32 |
ToInt32(p *int32) int32 |
| uint | Uint(v uint) *uint |
ToUint(p *uint) uint |
| uint8 | Uint8(v uint8) *uint8 |
ToUint8(p *uint8) uint8 |
| uint16 | Uint16(v uint16) *uint16 |
ToUint16(p *uint16) uint16 |
| uint32 | Uint32(v uint32) *uint32 |
ToUint32(p *uint32) uint32 |
| uint64 | Uint64(v uint64) *uint64 |
ToUint64(p *uint64) uint64 |
| float32 | Float32(v float32) *float32 |
ToFloat32(p *float32) float32 |
| byte | Byte(v byte) *byte |
ToByte(p *byte) byte |
| rune | Rune(v rune) *rune |
ToRune(p *rune) rune |
| uintptr | Uintptr(v uintptr) *uintptr |
ToUintptr(p *uintptr) uintptr |
| Type | Create | Dereference |
|---|---|---|
| time.Time | Time(v time.Time) *time.Time |
ToTime(p *time.Time) time.Time |
| time.Duration | Duration(v time.Duration) *time.Duration |
ToDuration(p *time.Duration) time.Duration |
| complex64 | Complex64(v complex64) *complex64 |
ToComplex64(p *complex64) complex64 |
| complex128 | Complex128(v complex128) *complex128 |
ToComplex128(p *complex128) complex128 |
For each type, both slice conversion functions are available:
| Type | To Slice | From Slice |
|---|---|---|
| string | StringSlice([]string) []*string |
ToStringSlice([]*string) []string |
| int | IntSlice([]int) []*int |
ToIntSlice([]*int) []int |
| int8 | Int8Slice([]int8) []*int8 |
ToInt8Slice([]*int8) []int8 |
| int16 | Int16Slice([]int16) []*int16 |
ToInt16Slice([]*int16) []int16 |
| int32 | Int32Slice([]int32) []*int32 |
ToInt32Slice([]*int32) []int32 |
| int64 | Int64Slice([]int64) []*int64 |
ToInt64Slice([]*int64) []int64 |
| uint | UintSlice([]uint) []*uint |
ToUintSlice([]*uint) []uint |
| uint8 | Uint8Slice([]uint8) []*uint8 |
ToUint8Slice([]*uint8) []uint8 |
| uint16 | Uint16Slice([]uint16) []*uint16 |
ToUint16Slice([]*uint16) []uint16 |
| uint32 | Uint32Slice([]uint32) []*uint32 |
ToUint32Slice([]*uint32) []uint32 |
| uint64 | Uint64Slice([]uint64) []*uint64 |
ToUint64Slice([]*uint64) []uint64 |
| float32 | Float32Slice([]float32) []*float32 |
ToFloat32Slice([]*float32) []float32 |
| float64 | Float64Slice([]float64) []*float64 |
ToFloat64Slice([]*float64) []float64 |
| bool | BoolSlice([]bool) []*bool |
ToBoolSlice([]*bool) []bool |
| byte | ByteSlice([]byte) []*byte |
ToByteSlice([]*byte) []byte |
| time.Time | TimeSlice([]time.Time) []*time.Time |
ToTimeSlice([]*time.Time) []time.Time |
| time.Duration | DurationSlice([]time.Duration) []*time.Duration |
ToDurationSlice([]*time.Duration) []time.Duration |
For each type, both map conversion functions are available (all maps use string keys):
| Type | To Map | From Map |
|---|---|---|
| string | StringMap(map[string]string) map[string]*string |
ToStringMap(map[string]*string) map[string]string |
| int | IntMap(map[string]int) map[string]*int |
ToIntMap(map[string]*int) map[string]int |
| int8 | Int8Map(map[string]int8) map[string]*int8 |
ToInt8Map(map[string]*int8) map[string]int8 |
| int16 | Int16Map(map[string]int16) map[string]*int16 |
ToInt16Map(map[string]*int16) map[string]int16 |
| int32 | Int32Map(map[string]int32) map[string]*int32 |
ToInt32Map(map[string]*int32) map[string]int32 |
| int64 | Int64Map(map[string]int64) map[string]*int64 |
ToInt64Map(map[string]*int64) map[string]int64 |
| uint | UintMap(map[string]uint) map[string]*uint |
ToUintMap(map[string]*uint) map[string]uint |
| uint8 | Uint8Map(map[string]uint8) map[string]*uint8 |
ToUint8Map(map[string]*uint8) map[string]uint8 |
| uint16 | Uint16Map(map[string]uint16) map[string]*uint16 |
ToUint16Map(map[string]*uint16) map[string]uint16 |
| uint32 | Uint32Map(map[string]uint32) map[string]*uint32 |
ToUint32Map(map[string]*uint32) map[string]uint32 |
| uint64 | Uint64Map(map[string]uint64) map[string]*uint64 |
ToUint64Map(map[string]*uint64) map[string]uint64 |
| float32 | Float32Map(map[string]float32) map[string]*float32 |
ToFloat32Map(map[string]*float32) map[string]float32 |
| float64 | Float64Map(map[string]float64) map[string]*float64 |
ToFloat64Map(map[string]*float64) map[string]float64 |
| bool | BoolMap(map[string]bool) map[string]*bool |
ToBoolMap(map[string]*bool) map[string]bool |
| byte | ByteMap(map[string]byte) map[string]*byte |
ToByteMap(map[string]*byte) map[string]byte |
| time.Time | TimeMap(map[string]time.Time) map[string]*time.Time |
ToTimeMap(map[string]*time.Time) map[string]time.Time |
| time.Duration | DurationMap(map[string]time.Duration) map[string]*time.Duration |
ToDurationMap(map[string]*time.Duration) map[string]time.Duration |
The package has minimal overhead with most operations optimized to near-zero cost by the Go compiler.
BenchmarkTo-12 1000000000 0.12 ns/op 0 B/op 0 allocs/op
BenchmarkFrom-12 1000000000 0.15 ns/op 0 B/op 0 allocs/op
BenchmarkFromOr-12 1000000000 0.11 ns/op 0 B/op 0 allocs/op
BenchmarkEqual-12 1000000000 0.13 ns/op 0 B/op 0 allocs/op
BenchmarkCopy-12 1000000000 0.14 ns/op 0 B/op 0 allocs/op
BenchmarkIsNil-12 1000000000 0.12 ns/op 0 B/op 0 allocs/op
BenchmarkOr-12 1000000000 0.13 ns/op 0 B/op 0 allocs/op
BenchmarkFilter-12 1000000000 0.18 ns/op 0 B/op 0 allocs/op
BenchmarkNonZero-12 1000000000 0.15 ns/op 0 B/op 0 allocs/op
BenchmarkIsZero-12 1000000000 0.14 ns/op 0 B/op 0 allocs/op
BenchmarkSwap-12 1000000000 0.16 ns/op 0 B/op 0 allocs/op
BenchmarkModify-12 1000000000 0.19 ns/op 0 B/op 0 allocs/op
BenchmarkString-12 1000000000 0.11 ns/op 0 B/op 0 allocs/op
BenchmarkToString-12 1000000000 0.12 ns/op 0 B/op 0 allocs/op
BenchmarkInt-12 1000000000 0.10 ns/op 0 B/op 0 allocs/op
BenchmarkToInt-12 1000000000 0.11 ns/op 0 B/op 0 allocs/op
Key Performance Characteristics:
- Sub-nanosecond operations - All functions complete in ~0.1-0.2 ns
- Zero allocations - No heap allocations for pointer operations
- Compiler optimized - Functions are typically inlined by the Go compiler
- Production ready - Performance suitable for hot code paths
- Constant time - All operations are O(1) complexity
Run benchmarks yourself:
# Run all benchmarks with memory statistics
go test -bench=. -benchmem
# Run specific benchmark
go test -bench=BenchmarkTo -benchmem
# Compare with baseline (create baseline first)
go test -bench=. -benchmem > new.txt
benchstat old.txt new.txtUse pointers for:
- Optional fields in structs (especially for JSON/API models)
- Distinguishing between "not set" and "zero value"
- Database NULL values
- Configuration with multiple precedence levels
- Large structs to avoid copying overhead
Avoid pointers for:
- Small, frequently-accessed values (int, bool) in hot paths
- Values that should never be nil
- Internal function parameters unless needed for mutation
- Simple data without optional semantics
// Use From() when zero value is acceptable
age := ptr.From(user.Age) // 0 if nil
// Use FromOr() when you need a specific default
timeout := ptr.FromOr(config.Timeout, 30*time.Second)
// Use MustFrom() only when nil indicates a programmer error
// (not for user input or external data)
id := ptr.MustFrom(user.ID) // Panic if nil = bug in code// Prefer type-specific functions for common types (better IDE support)
name := ptr.String("Alice") // ✅ Clear and autocomplete-friendly
age := ptr.Int(30)
// Use generics for custom types or when type-agnostic code is needed
type UserID int64
id := ptr.To[UserID](12345) // ✅ Works with custom types
func GetValue[T any](p *T, defaultVal T) T {
return ptr.FromOr(p, defaultVal) // ✅ Generic function
}// Anti-pattern: Silently converting errors to nil
func getUser(id int) *User {
user, err := db.GetUser(id)
if err != nil {
return nil // ❌ Lost error information
}
return &user
}
// Better: Return both value and error
func getUser(id int) (*User, error) {
user, err := db.GetUser(id)
if err != nil {
return nil, err // ✅ Preserve error
}
return &user, nil
}
// When using ptr for optional values, consider validation
func updateUser(req UpdateUserRequest) error {
if !ptr.IsNil(req.Email) {
email := ptr.ToString(req.Email)
if !isValidEmail(email) {
return errors.New("invalid email") // ✅ Validate before use
}
}
return nil
}// Each pointer adds indirection and potential cache misses
// For hot paths with small values, benchmark both approaches
// Approach 1: Pointers (flexible, optional semantics)
type Config struct {
MaxRetries *int
Timeout *time.Duration
}
// Approach 2: Values with sentinel (no indirection, better cache locality)
type Config struct {
MaxRetries int // 0 or -1 means "not set"
Timeout time.Duration
}
// Choose based on your use case:
// - API/JSON models: Pointers (distinguish null from zero)
// - Internal hot paths: Values (better performance)
// - Configuration: Pointers (clear optional semantics)A: Use this package when you need:
- Pointers to literals:
ptr.Int(42)vs the verbose two-line alternative - Safe nil handling:
ptr.From(x)vs manual nil checks - Optional API fields: Clear distinction between "not provided" and "zero value"
Standard Go is fine when:
- You don't need pointers at all
- You're comfortable with manual nil checks
- You prefer zero dependencies (though this package has zero deps too)
A: Yes. The package is:
- Battle-tested in production environments
- Fully tested with comprehensive coverage
- Zero external dependencies
- Simple, focused API with no surprises
- Performance-optimized (sub-nanosecond operations)
A: Compare:
// Manual nil check (verbose, repetitive)
var name string
if user.Name != nil {
name = *user.Name
}
// name is "" if user.Name was nil
// With ptr (concise, clear intent)
name := ptr.From(user.Name)The benefit increases with multiple optional fields.
A: Both approaches are valid:
// Using sql.NullString
type User struct {
Email sql.NullString
}
// Pros: Standard library, clear database intent
// Cons: Verbose to work with, doesn't play well with JSON
// Using *string with ptr
type User struct {
Email *string `json:"email,omitempty"`
}
// Pros: Works with JSON, APIs, and databases
// Cons: Need to handle conversion for SQL
// You can combine both:
func (u User) ToSQL() UserDB {
return UserDB{
Email: sql.NullString{
String: ptr.ToString(u.Email),
Valid: !ptr.IsNil(u.Email),
},
}
}A: Requires Go 1.18+ for generics. For older versions:
- Use type-specific functions (they don't require generics)
- Or stick with Go 1.17 patterns (manual pointer handling)
A: This package focuses on:
- Simplicity: Small, focused API
- Performance: Zero allocation, compiler-optimized
- Type safety: Leverages Go generics
- Zero dependencies: Pure standard library
Similar packages may offer different trade-offs. This package prioritizes simplicity and performance.
A: Yes, pointers created by this package are regular Go pointers:
type User struct {
Name *string `json:"name,omitempty"`
}
// JSON unmarshaling works normally
json.Unmarshal(data, &user)
// Creating for marshaling
user := User{Name: ptr.String("Alice")}
json.Marshal(user) // {"name":"Alice"}
user.Name = nil
json.Marshal(user) // {} - field omittedThis package follows Semantic Versioning:
- v1.x.x: Current stable version with backward compatibility guarantee
- No breaking changes in minor or patch releases
- New features added in minor versions (v1.1.0, v1.2.0, etc.)
- Bug fixes in patch versions (v1.0.1, v1.0.2, etc.)
The public API is stable and production-ready. We're committed to maintaining backward compatibility.
Apache License 2.0 - see LICENSE file for details.
We welcome contributions! Here's how you can help:
- Check if the issue already exists before creating a new one
- Include Go version, operating system, and minimal reproduction code
- Describe expected vs actual behavior
Before submitting a PR:
- Write tests - Ensure your changes are covered by tests
- Run tests -
go test -v ./... - Run benchmarks -
go test -bench=. -benchmem(if performance-related) - Format code -
go fmt ./... - Update documentation - Keep README and godoc comments current
# Clone the repository
git clone https://github.com/companyinfo/ptr.git
cd ptr
# Run tests
go test -v
# Run benchmarks
go test -bench=. -benchmem
# Run with coverage
go test -coverprofile=coverage.out
go tool cover -html=coverage.out- Follow Go best practices and idioms
- Maintain backward compatibility unless it's a major version bump
- Keep functions simple and focused
- Add godoc comments for public functions
- Ensure zero allocations for performance-critical functions
- Bug fixes with test cases
- Performance improvements with benchmarks
- Documentation improvements
- Additional type-specific helpers (if commonly needed)
- Real-world use case examples
- Breaking changes without discussion
- Features that add external dependencies
- Complex features that increase API surface unnecessarily
Documentation · Report Bug · Request Feature
Made with ❤️ by the CompanyInfo team