Skip to content

alifengineer/pii

Repository files navigation

pii — PII Masking & Sanitization for Go

CI cov Go Reference License: MIT

English | Русский

A Go library for masking personally identifiable information (PII) in logs and serialized output. Two complementary approaches that can be used independently or together:

  • Runtime masking — tag struct fields and call pii.Mask(v). Simple, dynamic, uses reflection.
  • Code generation — generate static Sanitize() methods at build time. Zero reflection at runtime, faster on hot paths.

Install

As a library:

go get github.com/alifengineer/pii

As a code generator (binary):

go install github.com/alifengineer/pii/cmd/sanitizer@latest

Make sure $(go env GOPATH)/bin is in your PATH so go generate can find the sanitizer binary.


Approach 1: Runtime masking with struct tags

Tag the fields you want masked, then call pii.Mask:

package main

import (
    "fmt"
    "github.com/alifengineer/pii"
)

type User struct {
    ID    int
    Name  string `pii:"name"`
    Phone string `pii:"phone"`
    Email string `pii:"email"`
    PAN   string `pii:"pan"`
}

func main() {
    u := User{
        ID:    42,
        Name:  "Alice Smith",
        Phone: "+12025550199",
        Email: "alice@example.com",
        PAN:   "4111111111111111",
    }
    fmt.Println(pii.Mask(u))
    // {42 A**** S**** ***0199 a***@e***.com 411111******1111}
}

pii.Mask(v any) any returns a deep copy with masked fields — your original value is never mutated. Nested structs and pointers are walked recursively.

Built-in tag values

Tag Constant Behavior Example
pii:"name" pii.Name Keeps the first rune of each word Alice SmithA**** S****
pii:"phone" pii.Phone Keeps the last 4 characters +12025550199***0199
pii:"email" pii.Email Keeps the first char of local part and domain prefix, preserves TLD alice@example.coma***@e***.com
pii:"pan" pii.PAN Keeps the first 6 (BIN) and last 4 — PCI-DSS safe 4111111111111111411111******1111

Custom maskers

Register your own masker for any tag value:

pii.Register("ssn", func(v string) string {
    if len(v) < 4 {
        return "****"
    }
    return "***-**-" + v[len(v)-4:]
})

type Person struct {
    SSN string `pii:"ssn"`
}

The masker signature is func(value string) string (pii.Masker).

Generic builtin functions

If you want to call the masking functions directly (without going through Mask), they are exposed as generics over ~string:

type MyEmail string

masked := pii.MaskEmail(MyEmail("alice@example.com"))
// "a***@e***.com" of type MyEmail

Available: MaskName, MaskPhone, MaskEmail, MaskPAN.


Approach 2: Code generation with //go:generate

Instead of paying the reflection cost on every call, generate static Sanitize() methods at build time.

Quick start

Define a sanitizable type by giving it a Sanitize() method that returns itself:

package user

//go:generate sanitizer

type Email string

func (e Email) Sanitize() Email {
    // your masking logic
    return Email("...masked...")
}

type User struct {
    ID    int
    Email Email
}

Run:

go generate ./...

The generator creates user_sanitize.go next to your source:

// Code generated by sanitizer; DO NOT EDIT.
package user

func (v User) Sanitize() User {
    v.Email = v.Email.Sanitize()
    return v
}

Use it:

log.Println(u.Sanitize())

How it detects sanitizable fields

The generator inspects every struct field. If the field's type has a Sanitize() T method, the generated code calls it. Otherwise the field is copied as-is. Supported field shapes:

  • Direct values: Field Tv.Field = v.Field.Sanitize()
  • Pointers: Field *T → nil-safe call
  • Slices: Field []T → loop over elements
  • Pointer slices: Field []*T → loop with nil check
  • Nested structs: dependency types are auto-discovered and get their own Sanitize() generated

Flags

Flag Description
-type=Name1,Name2 Generate only for the named structs and their transitive dependencies. Without this flag, all qualifying structs in the package are generated.
-destination=path Output file path. Default: <source>_sanitize.go
-check Compare output to the existing file, exit non-zero if it would differ. Use this in CI to ensure generated files are committed.
-v Print per-field reasoning and auto-included dependencies to stderr
-debug Dump the parsed model as JSON to stdout, without generating files

The generator must be invoked via go generate — it relies on the GOFILE and GOPACKAGE environment variables that go generate sets automatically.

Example: combining with built-in maskers

You can write Sanitize() on a custom string type by reusing the built-in generic maskers:

package user

import "github.com/alifengineer/pii"

//go:generate sanitizer

type Email string
func (e Email) Sanitize() Email { return pii.MaskEmail(e) }

type Phone string
func (p Phone) Sanitize() Phone { return pii.MaskPhone(p) }

type User struct {
    ID    int
    Email Email
    Phone Phone
}

After go generate, User.Sanitize() exists and calls the right masker for each field — no reflection, no tags, just direct method calls.


Which approach should I use?

Use runtime (pii.Mask) when... Use codegen when...
You want minimal setup You care about performance (hot paths, high RPS)
Your types live in third-party packages you can't add methods to You can own the types
You're prototyping or scripting You want compile-time safety — newly added fields are visible in the diff
Reflection overhead is acceptable You want zero reflection at runtime

The two approaches are not mutually exclusive — use codegen on hot paths and pii.Mask for occasional or schema-less logging.


Contributing

Issues and PRs are welcome. See CONTRIBUTING.md.

License

MIT

About

Golang library for masking pii data from logs. Also, you can generate Sanitizer for your types.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors