deidentify package - github.com/aliengiraffe/deidentify - Go Packages
Help us shape the future of Go! Take the 2026 Go Developer Survey

deidentify

package module
v1.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 10 Imported by: 1

README

Deidentify

Go Reference codecov Version License Mentioned in Awesome Go

A Go library for detecting and removing personally identifiable information (PII) from text and structured data.

Overview

deidentify is an open source Go package, created by AlienGiraffe, Inc., that detects personally identifiable information in text and structured data and replaces it with format-preserving substitutes. Replacements are deterministic: the same input and secret key always produce the same output, so referential integrity is preserved across records and runs.

Features

  • Multiple PII types support: Emails, phone numbers, SSNs, credit cards, names, and addresses
  • Format preservation: Maintains the original data format for better usability
  • Deterministic replacements: Same inputs produce the same outputs for referential integrity
  • Context awareness: Uses column names as context to prevent correlation
  • Table processing: Handles structured data with type-aware deidentification
  • Thread-safe: Suitable for concurrent processing

Installation

go get github.com/aliengiraffe/deidentify

Usage

Basic Example
package main

import (
    "fmt"
    "log"
    
    "github.com/aliengiraffe/deidentify"
)

func main() {
    // Generate a secure secret key (or provide your own)
    secretKey, err := deidentify.GenerateSecretKey()
    if err != nil {
        log.Fatal("Failed to generate secret key:", err)
    }
    
    // Create a deidentifier instance
    d := deidentify.NewDeidentifier(secretKey)
    
    // Deidentify text containing PII
    text := `Contact Frodo Baggins at frodo.baggins@shire.me or (555) 123-4567.
His SSN is 123-45-6789 and he lives at 1 Bagshot Row, Hobbiton.`

    redacted, err := d.Text(text)
    if err != nil {
        log.Fatal("Failed to deidentify text:", err)
    }
    
    fmt.Println(redacted)
    // Output example:
    // Contact Taylor Miller at member4921@demo.co or (555) 642-8317.
    // His SSN is 304-51-9872 and he lives at 2845 Oak Ave.
}
Processing Structured Data
package main

import (
    "fmt"
    "log"
    
    "github.com/aliengiraffe/deidentify"
)

func main() {
    secretKey, err := deidentify.GenerateSecretKey()
    if err != nil {
        log.Fatal("Failed to generate secret key:", err)
    }
    
    d := deidentify.NewDeidentifier(secretKey)
    
    // Create a table with PII data
    table := &deidentify.Table{
        Columns: []deidentify.Column{
            {
                Name:     "customer_name",
                DataType: deidentify.TypeName,
                Values:   []interface{}{"Gandalf Grey", "Aragorn Strider", nil},
            },
            {
                Name:     "email",
                DataType: deidentify.TypeEmail,
                Values:   []interface{}{"mithrandir@wizard.com", "ranger@gondor.me", ""},
            },
        },
    }
    
    // Deidentify the table
    result, err := d.Table(table)
    if err != nil {
        log.Fatal("Failed to deidentify table:", err)
    }
    
    // Process the result
    for i, col := range result.Columns {
        fmt.Printf("Column: %s\n", col.Name)
        for j, val := range col.Values {
            fmt.Printf("  [%d]: %v\n", j, val)
        }
    }
}
Processing Slice Data
// Deidentify [][]string data (CSV-like format)
data := [][]string{
    {"Alice Johnson", "alice@example.com", "555-123-4567"},
    {"Bob Smith", "bob@company.org", "(555) 987-6543"},
}

// Option 1: Automatic type inference (recommended)
result, err := d.Slices(data)
if err != nil {
    log.Fatal("Failed to deidentify:", err)
}
// Types are automatically detected: Name, Email, Phone
// Result: [["Taylor Miller", "user4921@demo.co", "555-642-8317"], ...]

// Option 2: Explicit column types only
columnTypes := []deidentify.DataType{deidentify.TypeName, deidentify.TypeEmail, deidentify.TypePhone}
result, err = d.Slices(data, columnTypes)

// Option 3: Both explicit types and custom column names
columnNames := []string{"customer_name", "customer_email", "customer_phone"}
result, err = d.Slices(data, columnTypes, columnNames)

More Examples

See the examples directory for additional usage patterns:

Configuration

The deidentify package uses a deterministic approach for consistency. The secret key provides the randomness source, making the anonymization both reproducible and secure.

Supported PII Types

PII Type Description Example Input Example Output
TypeName Personal names Bilbo Baggins Taylor Miller
TypeEmail Email addresses bilbo@bag-end.shire user4921@demo.co
TypePhone Phone numbers (555) 123-4567 (555) 642-8317
TypeSSN Social Security Numbers 123-45-6789 304-51-9872
TypeCreditCard Credit card numbers 4111-1111-1111-1111 4000 8521 7694 3217
TypeAddress Street addresses Bag End, Bagshot Row 2845 Oak Ave

Security

While this library aims to detect common PII patterns, no automated system can guarantee 100% detection. Always verify the results in sensitive applications.

Note: By default, the library preserves area codes in phone numbers for better usability, as they often indicate geographic regions rather than individuals. Consider your specific requirements when implementing.

Data Variety

The replacement tables contain:

  • 110+ gender-neutral first names
  • 130+ last names
  • 105+ fictional email domains
  • 100+ email username patterns
  • 120+ street name variations with international formats

A larger replacement space reduces the likelihood that patterns in the output can be traced back to the original values.

International Support

The library includes support for international address formats:

  • North American: US and Canadian style addresses
  • European: UK, French, German, Italian, Spanish, etc.
  • Asian: Japanese, Chinese, Southeast Asian formats
  • Middle Eastern and global formats

The detection patterns have been optimized to recognize common address structures across different languages and regional conventions, while the anonymization preserves format and readability.

Releases

Creating a New Release

The library uses GitHub Actions to automate the release process. To create a new release:

  1. Update your code and commit all changes
  2. Create and push a new tag with semantic versioning format:
    git tag v1.0.0
    git push origin v1.0.0
    
  3. The GitHub Actions workflow will automatically:
    • Run tests to ensure everything works
    • Generate a changelog based on commits since the last tag
    • Create a GitHub release with documentation
    • Publish the new version to the Go module proxy

This makes the new version immediately available for users to install via go get github.com/aliengiraffe/deidentify@v1.0.0.

Performance

To run performance benchmarks:

# Run all benchmarks
go test -bench=. -benchtime=10s

# Run only the paragraph deidentification benchmark
go test -bench=BenchmarkParagraphDeidentification -benchtime=1x

# Run benchmarks with memory allocation stats
go test -bench=. -benchmem

# Run parallel benchmarks to test concurrent performance
go test -bench=BenchmarkParagraphDeidentificationParallel
CPU and Memory Profiling with pprof

For detailed performance analysis, you can use pprof to profile CPU usage and memory allocations:

# Generate CPU profile
go test -bench=BenchmarkParagraphDeidentification -cpuprofile=cpu.prof -benchtime=10s

# Generate memory profile
go test -bench=BenchmarkParagraphDeidentification -memprofile=mem.prof -benchtime=10s

# Analyze CPU profile in terminal
go tool pprof cpu.prof
# Then use interactive commands like 'top', 'list', 'web'

# Analyze memory profile in terminal
go tool pprof mem.prof
Interactive Web UI

pprof's built-in web server provides an interactive visualization of a profile:

# Start interactive web UI for CPU profile (opens browser automatically)
go tool pprof -http=:8080 cpu.prof

# Start interactive web UI for memory profile on different port
go tool pprof -http=:8081 mem.prof

# If browser doesn't open automatically, navigate to:
# http://localhost:8080 (for CPU)
# http://localhost:8081 (for memory)

The web UI provides:

  • Flame Graph: Interactive flame graph showing call stack and CPU/memory usage
  • Graph View: Call graph with edges showing relationships and costs
  • Top View: Sorted list of functions by resource consumption
  • Source View: Line-by-line annotation of source code with costs
  • Peek View: Shows callers and callees of selected functions
  • Disassembly View: Assembly-level analysis
Advanced Analysis
# Focus on specific functions (e.g., deidentify package)
go tool pprof -focus=deidentify cpu.prof

# Compare two profiles (e.g., before and after optimization)
go tool pprof -base=cpu_before.prof cpu_after.prof

# Generate a PDF report (requires graphviz)
go tool pprof -pdf cpu.prof > cpu_profile.pdf

# Filter by specific time range or samples
go tool pprof -show_from=Text -show=deidentify cpu.prof
Automated Profiling

For convenience, use the included profiling script:

./scripts/profile-benchmarks.sh

This script will:

  • Run benchmarks with CPU and memory profiling
  • Generate text reports (top consumers, full profiles)
  • Create visual graphs (SVG/PNG) if graphviz is installed
  • Save all artifacts in the profiles/ directory
CI/CD Integration

Pull requests automatically generate profiling reports through GitHub Actions. The workflow:

  • Runs benchmarks with CPU and memory profiling
  • Generates pprof reports and visualizations
  • Posts a summary comment on the PR with key metrics
  • Uploads full profiling artifacts for download

The benchmarks measure the time to deidentify paragraphs containing various types of PII. On modern hardware, the library can process over 600 paragraphs per second with an average processing time of ~1.5ms per paragraph.

Contributing

Contributions are welcome! Please read our Contributing Guidelines for detailed information on how to contribute to this project.

Quick start for contributors:

  1. Fork the repository and clone your fork
  2. Set up the development environment:
    ./scripts/setup-pre-commit-hook.sh
    go mod download
    
  3. Create your feature branch (git checkout -b feature/amazing-feature)
  4. Make your changes and ensure tests pass (go test ./...)
  5. Commit your changes (pre-commit hook will format code automatically)
  6. Push to your fork and submit a Pull Request

See CONTRIBUTING.md for detailed guidelines on code standards, testing, and the development workflow.

License

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

About

Created and maintained by AlienGiraffe, Inc.

Documentation

Overview

Package deidentify removes personally identifiable information (PII) from text and structured data while preserving the shape and utility of the original data.

Replacements are deterministic: the same input, deidentified with the same secret key, always yields the same output. This preserves referential integrity across records and runs, so joins and groupings still work on anonymized data. Different secret keys produce different outputs, so the mapping cannot be reproduced without the key.

Getting started

Create a Deidentifier with a secret key and call Deidentifier.Text to redact free-form text:

secretKey, err := deidentify.GenerateSecretKey()
if err != nil {
	log.Fatal(err)
}

d := deidentify.NewDeidentifier(secretKey)

redacted, err := d.Text("Contact Frodo Baggins at frodo@shire.me or (555) 123-4567.")
if err != nil {
	log.Fatal(err)
}
fmt.Println(redacted)

Supported PII types

The package detects and replaces names, email addresses, phone numbers, Social Security numbers, credit card numbers, and street addresses. Each has a corresponding DataType constant and a convenience method on Deidentifier for deidentifying a single value of a known type.

Replacements preserve the format of the original value. Phone numbers keep their punctuation and area code, credit card numbers keep a valid Luhn checksum, and SSNs keep a structurally valid area-group-serial layout.

Structured data

Deidentifier.Table processes column-oriented data where the type of each column is known, and Deidentifier.Slices processes CSV-like [][]string data, inferring the type of each column when it is not supplied. Both use the column name as part of the replacement context, so the same value appearing in two different columns maps to two different replacements. This prevents correlation across columns.

Values of type TypeGeneric are returned unchanged, which is how columns with no detected PII pass through the pipeline intact.

Concurrency

A Deidentifier is safe for concurrent use by multiple goroutines. Its internal mapping tables are guarded by a mutex.

Limitations

Detection is pattern-based. No automated system can guarantee that it finds every piece of PII, particularly in unstructured text, so results should be verified in sensitive applications. By default, phone area codes are preserved because they usually indicate a geographic region rather than an individual; consider whether that tradeoff suits your threat model.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateSecretKey

func GenerateSecretKey() (string, error)

GenerateSecretKey returns a cryptographically secure random 256-bit key, hex-encoded, suitable for passing to NewDeidentifier.

Store the key if you need replacements to stay consistent across runs: deidentifying the same data with a different key produces different output.

Example
package main

import (
	"fmt"
	"log"

	"github.com/aliengiraffe/deidentify"
)

func main() {
	secretKey, err := deidentify.GenerateSecretKey()
	if err != nil {
		log.Fatal(err)
	}

	// The key is a hex-encoded 256-bit value.
	fmt.Println(len(secretKey))
}
Output:
64

Types

type Column

type Column struct {
	// Name identifies the column and scopes its replacement mappings.
	Name string
	// DataType is the kind of PII stored in this column.
	DataType DataType
	// Values holds the column's cells. A nil element is passed through as nil.
	Values []interface{}
}

Column represents a single column in a Table: its name, the type of PII it holds, and its values. The name is used as replacement context, so identical values in differently named columns receive different replacements.

type DataType

type DataType int

DataType represents the type of personally identifiable information held by a value, and determines which replacement strategy is applied to it.

const (
	// TypeName is a personal name, replaced with a generated first and last name.
	TypeName DataType = iota
	// TypeEmail is an email address, replaced with a generated address at a fictional domain.
	TypeEmail
	// TypePhone is a phone number, replaced while preserving punctuation and area code.
	TypePhone
	// TypeSSN is a US Social Security number, replaced with a structurally valid number.
	TypeSSN
	// TypeCreditCard is a credit card number, replaced with a number that passes the Luhn check.
	TypeCreditCard
	// TypeAddress is a street address, replaced with a generated street number and name.
	TypeAddress
	// TypeGeneric marks a value with no detected PII. Such values are returned unchanged.
	TypeGeneric
)

The PII types recognized by this package. Each type has a replacement strategy that preserves the format of the original value.

type Deidentifier

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

Deidentifier replaces PII with deterministic, format-preserving substitutes. Create one with NewDeidentifier. A Deidentifier is safe for concurrent use by multiple goroutines.

func NewDeidentifier

func NewDeidentifier(secretKey string) *Deidentifier

NewDeidentifier returns a Deidentifier that derives its replacements from secretKey. Use GenerateSecretKey to create one, and reuse the same key whenever deidentified values must match across runs or datasets.

Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/aliengiraffe/deidentify"
)

func main() {
	// In production, generate a key once with GenerateSecretKey and store it.
	// Reusing the same key keeps replacements consistent across runs.
	d := deidentify.NewDeidentifier("example-secret-key")

	redacted, err := d.Text("Please email Frodo Baggins at frodo.baggins@shire.me or call (555) 123-4567.")
	if err != nil {
		log.Fatal(err)
	}

	// redacted reads, for example:
	// "Please email Marlo Cole at undefined6840@private.com or call (555) 777-3471."
	fmt.Println(strings.Contains(redacted, "Frodo Baggins"))
	fmt.Println(strings.Contains(redacted, "frodo.baggins@shire.me"))
	fmt.Println(strings.Contains(redacted, "(555) 123-4567"))
}
Output:
false
false
false

func (*Deidentifier) Address added in v0.9.2

func (d *Deidentifier) Address(address string) (string, error)

Address deidentifies a single street address, replacing it with a generated street number and name. It recognizes international address formats and preserves a leading label, so "Europe HQ: 10 Rue de Rivoli, Paris, France" keeps its "Europe HQ:" prefix.

func (*Deidentifier) ClearMappings

func (d *Deidentifier) ClearMappings()

ClearMappings discards every stored value-to-replacement mapping. Because replacements are derived from the secret key, clearing does not change what a given input maps to; it only frees the accumulated mapping tables.

func (*Deidentifier) CreditCard added in v0.9.2

func (d *Deidentifier) CreditCard(cc string) (string, error)

CreditCard deidentifies a single credit card number. The replacement is a 16-digit test number that passes the Luhn checksum.

func (*Deidentifier) Email added in v0.9.2

func (d *Deidentifier) Email(email string) (string, error)

Email deidentifies a single email address, replacing it with a generated address at a fictional domain.

Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/aliengiraffe/deidentify"
)

func main() {
	d := deidentify.NewDeidentifier("example-secret-key")

	first, err := d.Email("bilbo@bag-end.shire")
	if err != nil {
		log.Fatal(err)
	}
	second, err := d.Email("bilbo@bag-end.shire")
	if err != nil {
		log.Fatal(err)
	}
	other, err := d.Email("frodo@shire.me")
	if err != nil {
		log.Fatal(err)
	}

	// The same address always maps to the same replacement, so records can
	// still be joined on the anonymized value.
	fmt.Println(first == second)
	fmt.Println(first == other)
	fmt.Println(strings.Contains(first, "@"))
}
Output:
true
false
true

func (*Deidentifier) Name added in v0.9.2

func (d *Deidentifier) Name(name string) (string, error)

Name deidentifies a single personal name, replacing it with a generated first and last name.

func (*Deidentifier) Phone added in v0.9.2

func (d *Deidentifier) Phone(phone string) (string, error)

Phone deidentifies a single phone number, preserving its punctuation and area code. Input that cannot be parsed as a phone number is replaced with an opaque generic value.

Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/aliengiraffe/deidentify"
)

func main() {
	d := deidentify.NewDeidentifier("example-secret-key")

	redacted, err := d.Phone("(555) 123-4567")
	if err != nil {
		log.Fatal(err)
	}

	// Punctuation and area code are preserved; only the subscriber digits change.
	fmt.Println(strings.HasPrefix(redacted, "(555) "))
	fmt.Println(len(redacted) == len("(555) 123-4567"))
	fmt.Println(redacted == "(555) 123-4567")
}
Output:
true
true
false

func (*Deidentifier) SSN added in v0.9.2

func (d *Deidentifier) SSN(ssn string) (string, error)

SSN deidentifies a single US Social Security number, replacing it with a structurally valid number in AAA-GG-SSSS form.

func (*Deidentifier) Slices added in v0.9.2

func (d *Deidentifier) Slices(data [][]string, optional ...interface{}) ([][]string, error)

Slices deidentifies CSV-like data, where each inner slice is a row. It returns a new [][]string of the same shape; empty cells are left empty.

Two optional arguments may follow data, in order:

  • columnTypes ([]DataType): the type of each column. When omitted, the type of every column is inferred by sampling its values.
  • columnNames ([]string): the name of each column, used as replacement context. When omitted, names of the form "column_0" are generated.

Both must have one entry per column, or Slices returns an error. Passing an argument of any other type is also an error.

d.Slices(data)
d.Slices(data, columnTypes)
d.Slices(data, columnTypes, columnNames)
Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/aliengiraffe/deidentify"
)

func main() {
	d := deidentify.NewDeidentifier("example-secret-key")

	data := [][]string{
		{"Gandalf Grey", "mithrandir@wizard.com", "555-123-4567"},
		{"Aragorn Strider", "ranger@gondor.me", "(555) 987-6543"},
	}

	// With no column types supplied, each column's type is inferred.
	result, err := d.Slices(data)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(len(result), len(result[0]))
	fmt.Println(result[0][1] == "mithrandir@wizard.com")
	fmt.Println(strings.Contains(result[0][1], "@"))
}
Output:
2 3
false
true
Example (ExplicitTypes)
package main

import (
	"fmt"
	"log"

	"github.com/aliengiraffe/deidentify"
)

func main() {
	d := deidentify.NewDeidentifier("example-secret-key")

	data := [][]string{
		{"Gandalf Grey", "mithrandir@wizard.com"},
		{"Aragorn Strider", "ranger@gondor.me"},
	}

	// Supplying types skips inference; supplying names controls the context
	// used for replacement, so the same value in two columns differs.
	columnTypes := []deidentify.DataType{deidentify.TypeName, deidentify.TypeEmail}
	columnNames := []string{"customer_name", "customer_email"}

	result, err := d.Slices(data, columnTypes, columnNames)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result[0][0] == "Gandalf Grey")
	fmt.Println(len(result))
}
Output:
false
2

func (*Deidentifier) Table added in v0.9.2

func (d *Deidentifier) Table(table *Table) (*Table, error)

Table deidentifies every column of a table according to its declared DataType, returning a new Table and leaving the input untouched. Nil values are preserved as nil.

Example
package main

import (
	"fmt"
	"log"

	"github.com/aliengiraffe/deidentify"
)

func main() {
	d := deidentify.NewDeidentifier("example-secret-key")

	table := &deidentify.Table{
		Columns: []deidentify.Column{
			{
				Name:     "customer_name",
				DataType: deidentify.TypeName,
				Values:   []interface{}{"Gandalf Grey", nil},
			},
			{
				Name:     "email",
				DataType: deidentify.TypeEmail,
				Values:   []interface{}{"mithrandir@wizard.com", ""},
			},
		},
	}

	result, err := d.Table(table)
	if err != nil {
		log.Fatal(err)
	}

	// Nil and empty values pass through untouched.
	fmt.Println(result.Columns[0].Values[0] == "Gandalf Grey")
	fmt.Println(result.Columns[0].Values[1] == nil)
	fmt.Println(result.Columns[1].Values[1] == "")
}
Output:
false
true
true

func (*Deidentifier) Text added in v0.9.2

func (d *Deidentifier) Text(text string) (string, error)

Text finds and replaces PII in free-form text, returning the redacted text. It detects email addresses, phone numbers, Social Security numbers, credit card numbers, names, and street addresses. Detection is pattern-based and is not guaranteed to be exhaustive.

type Table

type Table struct {
	// Columns are the table's columns, each carrying its own data type.
	Columns []Column
}

Table represents column-oriented data to be deidentified as a unit.

Directories

Path Synopsis
examples
basic command
international command
slices command
table command

Jump to

Keyboard shortcuts

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