Skip to content

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 →