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.
As a library:
go get github.com/alifengineer/piiAs a code generator (binary):
go install github.com/alifengineer/pii/cmd/sanitizer@latestMake sure $(go env GOPATH)/bin is in your PATH so go generate can find the sanitizer binary.
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.
| Tag | Constant | Behavior | Example |
|---|---|---|---|
pii:"name" |
pii.Name |
Keeps the first rune of each word | Alice Smith → A**** 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.com → a***@e***.com |
pii:"pan" |
pii.PAN |
Keeps the first 6 (BIN) and last 4 — PCI-DSS safe | 4111111111111111 → 411111******1111 |
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).
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 MyEmailAvailable: MaskName, MaskPhone, MaskEmail, MaskPAN.
Instead of paying the reflection cost on every call, generate static Sanitize() methods at build time.
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())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 T→v.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
| 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.
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.
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.
Issues and PRs are welcome. See CONTRIBUTING.md.