Credit Card Validator — Go source
Validate credit card numbers with the Luhn checksum and detect the issuing brand (Visa, Mastercard, Amex, Discover, Diners, JCB, UnionPay, Maestro). 100% client-side - nothing is transmitted.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package creditcardvalidator is the Go twin of CosmoDev's src/lib/creditCard.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// Pure + deterministic, never panics. The table-driven tests in
// creditcardvalidator_test.go share vectors with src/lib/creditCard.test.ts so the
// two implementations are held to the same contract.
//
// The lib sanitizes the input to digits, runs the Luhn checksum, detects the
// issuing brand by prefix + length, and groups the number for display. Every
// exported function mirrors its TS counterpart's algorithm and public surface.
package creditcardvalidator
import (
"regexp"
"slices"
"strings"
)
// CardInfo is the Go twin of the TS CardInfo interface. Brand is "" when the
// brand is unknown (the TS uses null); no real brand is ever the empty string.
type CardInfo struct {
Input string // the original input verbatim
Sanitized string // digits only
Valid bool // digits present, sane length, and Luhn passes
LuhnValid bool // Luhn checksum passes (and length >= 12, per validateCard)
Brand string // issuing brand, or "" when unknown
Formatted string // grouped per brand spacing
}
// brandRule pairs a brand name with its prefix pattern and accepted lengths.
// Order matters: the first rule whose pattern matches AND whose lengths contain
// the digit count wins — mirroring the TS detectBrand() test order.
type brandRule struct {
name string
re *regexp.Regexp
lengths []int
}
// brandRules is built once from the same prefix/length table as the TS lib's
// detectBrand() tests. Patterns are anchored to the start (^) exactly like the
// TS regexes; \d is written [0-9] (RE2 ASCII digits, identical for digit input).
var brandRules = func() []brandRule {
specs := []struct {
name string
pattern string
lengths []int
}{
{"Visa", `^4`, []int{13, 16, 19}},
{"Mastercard", `^(5[1-5]|2(2[2-9]|[3-6][0-9]|7[01]|720))`, []int{16}},
{"American Express", `^3[47]`, []int{15}},
{"Discover", `^(6011|65|64[4-9]|622(12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9([01][0-9]|2[0-5])))`, []int{16, 19}},
{"Diners Club", `^(30[0-5]|3095|36|38|39|54|55)`, []int{14, 16}},
{"JCB", `^35(2[89]|[3-8][0-9])`, []int{16, 17, 18, 19}},
{"UnionPay", `^62`, []int{16, 17, 18, 19}},
{"Maestro", `^(50|56|57|58|6)`, []int{12, 13, 14, 15, 16, 17, 18, 19}},
}
rules := make([]brandRule, len(specs))
for i, s := range specs {
rules[i] = brandRule{name: s.name, re: regexp.MustCompile(s.pattern), lengths: s.lengths}
}
return rules
}()
// nonDigit matches any non-ASCII-digit byte, mirroring the TS /\D+/g used to
// sanitize input and the /^\d*$/ guard inside luhnCheck.
var nonDigit = regexp.MustCompile(`[^0-9]`)
// LuhnCheck reports whether digits passes the Luhn checksum. Empty or non-digit
// input returns false. It is the Go twin of luhnCheck() in src/lib/creditCard.ts.
func LuhnCheck(digits string) bool {
if len(digits) == 0 {
return false
}
// /^\d*$/ guard: any non-digit byte fails the whole check.
for _, b := range []byte(digits) {
if b < '0' || b > '9' {
return false
}
}
sum := 0
shouldDouble := false
for i := len(digits) - 1; i >= 0; i-- {
d := int(digits[i] - '0')
if shouldDouble {
d *= 2
if d > 9 {
d -= 9
}
}
sum += d
shouldDouble = !shouldDouble
}
return sum%10 == 0
}
// DetectBrand detects the card brand by prefix + length. It returns "" when the
// brand is unknown. It is the Go twin of detectBrand() in src/lib/creditCard.ts.
func DetectBrand(digits string) string {
if digits == "" {
return ""
}
for _, rule := range brandRules {
if rule.re.MatchString(digits) && slices.Contains(rule.lengths, len(digits)) {
return rule.name
}
}
return ""
}
// FormatCard groups the number per brand spacing rules. It is the Go twin of
// formatCard() in src/lib/creditCard.ts: Amex → 4-6-5, 14-digit Diners Club →
// 4-6-4, everything else → groups of 4 separated by single spaces.
func FormatCard(digits, brand string) string {
if digits == "" {
return ""
}
if brand == "American Express" {
return group(digits, []int{4, 6, 5})
}
if brand == "Diners Club" && len(digits) == 14 {
return group(digits, []int{4, 6, 4})
}
return groupFours(digits)
}
// group slices digits into the given widths, joining the parts with single
// spaces and appending any trailing remainder. It mirrors group() in the TS lib.
func group(digits string, widths []int) string {
var parts []string
i := 0
n := len(digits)
for _, w := range widths {
if i >= n {
break
}
end := min(i+w, n)
parts = append(parts, digits[i:end])
i += w
}
if i < n {
parts = append(parts, digits[i:])
}
return strings.Join(parts, " ")
}
// groupFours inserts a single space every four characters — the generic case of
// the TS regex digits.replace(/(.{4})(?=.)/g, '$1 ').trim(), which never emits a
// leading or trailing space (the (?=.) lookahead suppresses the final group).
func groupFours(digits string) string {
var b strings.Builder
for i := 0; i < len(digits); i++ {
if i > 0 && i%4 == 0 {
b.WriteByte(' ')
}
b.WriteByte(digits[i])
}
return b.String()
}
// sanitizeDigits strips every non-digit byte, mirroring the TS
// (input || '').replace(/\D+/g, '').
func sanitizeDigits(input string) string {
return nonDigit.ReplaceAllString(input, "")
}
// ValidateCard validates a card number, always returning a fully populated
// CardInfo and never panicking. It is the Go twin of validateCard() in
// src/lib/creditCard.ts: valid === luhnValid, and luhnValid requires a sanitized
// length of at least 12 AND a passing Luhn checksum.
func ValidateCard(input string) CardInfo {
sanitized := sanitizeDigits(input)
brand := DetectBrand(sanitized)
luhnValid := len(sanitized) >= 12 && LuhnCheck(sanitized)
return CardInfo{
Input: input,
Sanitized: sanitized,
Valid: luhnValid,
LuhnValid: luhnValid,
Brand: brand,
Formatted: FormatCard(sanitized, brand),
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →