Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/countryphone"
Give it a number or a prefix, get back the country and the numbering-plan metadata that prefix belongs to. It follows the ITU-T E.123 and E.164 recommendations.
d := countryphone.New(nil) // embedded dataset
info, err := d.NumberInfo("442071234567")
Longest-prefix, because prefixes overlap
A hash map keyed on the calling code does not work for this, and the reason is that E.164 prefixes are not a flat namespace.
+1 is North America. +1242 is the Bahamas, +1246 Barbados, +1441 Bermuda: all sit underneath +1 and all need to resolve to themselves rather than to the United States. +7 covers Russia and Kazakhstan, split by the digits that follow. Prefix lengths run from one to four digits with no rule you can apply in advance.
The resolution has to be longest-prefix-wins, and a trie gives that directly. Walk the digits, remember the deepest node that carried a country, stop when the walk runs out. That is a numtrie underneath.
The consequence for callers is that NumberInfo accepts either a bare prefix or a full number. Extra digits past the matched prefix are simply not part of the walk.
Startup cost against lookup cost
| Operation | ns/op | B/op | allocs/op |
|---|---|---|---|
New (build the trie) | 126,117 | 218,001 | 2,772 |
NumberInfo, match | 53 | 80 | 2 |
NumberInfo, no match | 181 | 96 | 3 |
NumberType | 1.2 | 0 | 0 |
Measured with go test -bench on Go 1.27.1, linux/amd64, Intel Core i7-1260P. Run go test -bench=. ./pkg/countryphone/ to reproduce them on your own hardware; the absolute values will differ, the ratios much less so.
Construction costs 126 microseconds and nearly 2,800 allocations, once. After that a lookup is 53 nanoseconds, which is roughly 2,400 lookups to repay the build.
Build the resolver once at startup and keep it. *Data is read-only after construction, so a single instance serves every goroutine without a lock.
The no-match path costs three times a match, because it walks the full depth before concluding there is nothing there. A workload dominated by unmatched input is the slow case.
NumberType at 1.2 ns and zero allocations is an in-memory table lookup on an already-resolved record.
The embedded dataset, and replacing it
New(nil) uses a dataset compiled into the binary, so there is no file to ship, no network call, and no startup dependency.
Numbering plans change. Countries are assigned new codes, ranges get reallocated, and a compiled-in dataset is a snapshot of when the library was built. New therefore accepts an InData of your own, which covers three situations: a carrier-specific plan, a newer dataset than the release you are pinned to, and a curated subset where you only care about a handful of countries and want the trie smaller.
The same pattern appears in countrycode, which takes a custom []CountryData for the same reasons.
The metadata
NumberInfo returns a *NumInfo with two fields:
type NumInfo struct {
Type int // 1 landline, 2 mobile, 3 pager, 4 satellite,
// 5 special service, 6 virtual, 7 other
Geo []GeoInfo
}
type GeoInfo struct {
Alpha2 string // ISO-3166 alpha-2
Area string
Type int // 1 state, 2 province or territory,
// 3 nation or territory, 4 non-geographic, 5 other
}
Geo is a slice rather than a single value, which matters for shared plans. A prefix inside the North American Numbering Plan resolves to several entries, and an Alpha2 of one country is not the answer for +1. Code that reads Geo[0] and stops will be wrong for exactly the prefixes people get wrong by hand.
NumberType and AreaType expand those integer codes into names, returning an error for a code outside the known set rather than an empty string. A lookup failure is likewise an error, not a nil record, so an unrecognised prefix stays distinguishable from a prefix with no metadata attached.
Input runs through phonekeypad on the way in, so separators are dropped and alphabetic vanity characters fold to their digits before the trie walk.
Example
d := countryphone.New(nil)
info, err := d.NumberInfo("39")
fmt.Println(info, err)
Full source is in example_countryphone_test.go. More runnable examples are on pkg.go.dev.
Dependencies
This package reaches no external module: it uses only the Go standard library, plus numtrie for the prefix index and phonekeypad for digit folding.
Links
- Reference documentation
- Source code
- The trie underneath: numtrie
- Country metadata by ISO code: countrycode
- All nurago packages