Last active
March 5, 2026 07:22
-
-
Save azhai/f4415589cf3f9e3f3c346648bfcf225b to your computer and use it in GitHub Desktop.
ToSnakeCase converts camelCase or PascalCase to snake_case in golang v1.24+
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // filename: snake_case.go | |
| package utils | |
| import ( | |
| "slices" | |
| "strings" | |
| "unicode" | |
| ) | |
| // ToSnakeCase converts camelCase or PascalCase to snake_case | |
| func ToSnakeCase(word string) string { | |
| var prev []rune | |
| b := strings.Builder{} | |
| prevUp, currUp := false, false | |
| for i, letter := range word { | |
| if currUp = unicode.IsUpper(letter); currUp { | |
| letter = unicode.ToLower(letter) | |
| } | |
| if prevUp { // cache to varibale named prev | |
| if n := len(prev); n > 0 && !currUp { | |
| prev = slices.Insert(prev, n-1, '_') | |
| } | |
| prev = append(prev, letter) | |
| } else { // write to the result and clear prev | |
| b.WriteString(string(prev)) | |
| if currUp && i > 0 { | |
| b.WriteRune('_') | |
| } | |
| b.WriteRune(letter) | |
| prev = prev[:0] | |
| } | |
| prevUp = currUp | |
| } | |
| b.WriteString(string(prev)) | |
| return b.String() | |
| } | |
| // ToSnakeCaseV0 converts camelCase or PascalCase to snake_case in early version of golang | |
| func ToSnakeCaseV0(word string) string { | |
| var prev, result []byte | |
| prevUp, currUp := false, false | |
| for i := 0; i < len(word); i++ { | |
| letter := word[i] | |
| if letter < 32 || letter > 126 { // It is NOT visible character | |
| continue | |
| } | |
| if letter >= 'A' && letter <= 'Z' { | |
| letter, currUp = letter+('a'-'A'), true | |
| } | |
| if prevUp { // cache to varibale named prev | |
| if n := len(prev); n > 0 && !currUp { | |
| prev = append(prev[:n-1], '_', prev[n-1]) | |
| } | |
| prev = append(prev, letter) | |
| } else { // write to the result and clear prev | |
| result = append(result, prev...) | |
| if currUp && i > 0 { | |
| result = append(result, '_') | |
| } | |
| result = append(result, letter) | |
| prev = prev[:0] | |
| } | |
| prevUp = currUp | |
| } | |
| result = append(result, prev...) | |
| return string(result) | |
| } | |
| // filename: snake_case_test.go | |
| package utils | |
| import ( | |
| "testing" | |
| ) | |
| func TestToSnakeCase(t *testing.T) { | |
| testCases := []struct { | |
| name string | |
| input string | |
| expected string | |
| }{ | |
| { | |
| name: "single lowercase letter", | |
| input: "a", | |
| expected: "a", | |
| }, | |
| { | |
| name: "single uppercase letter", | |
| input: "A", | |
| expected: "a", | |
| }, | |
| { | |
| name: "two letters camelCase", | |
| input: "aB", | |
| expected: "a_b", | |
| }, | |
| { | |
| name: "two letters uppercase", | |
| input: "AB", | |
| expected: "ab", | |
| }, | |
| { | |
| name: "single word lowercase", | |
| input: "user", | |
| expected: "user", | |
| }, | |
| { | |
| name: "single word uppercase", | |
| input: "USER", | |
| expected: "user", | |
| }, | |
| { | |
| name: "all uppercase", | |
| input: "URL", | |
| expected: "url", | |
| }, | |
| { | |
| name: "all uppercase longer", | |
| input: "HTTP", | |
| expected: "http", | |
| }, | |
| { | |
| name: "simple camelCase", | |
| input: "userName", | |
| expected: "user_name", | |
| }, | |
| { | |
| name: "camelCase with multiple words", | |
| input: "getUserName", | |
| expected: "get_user_name", | |
| }, | |
| { | |
| name: "camelCase ending with uppercase", | |
| input: "userID", | |
| expected: "user_id", | |
| }, | |
| { | |
| name: "PascalCase", | |
| input: "UserName", | |
| expected: "user_name", | |
| }, | |
| { | |
| name: "camelCase with consecutive uppercase", | |
| input: "parseXMLData", | |
| expected: "parse_xml_data", | |
| }, | |
| { | |
| name: "PascalCase with consecutive uppercase", | |
| input: "XMLParser", | |
| expected: "xml_parser", | |
| }, | |
| { | |
| name: "mixed case with numbers", | |
| input: "user123", | |
| expected: "user123", | |
| }, | |
| { | |
| name: "already snake_case", | |
| input: "user_name", | |
| expected: "user_name", | |
| }, | |
| // { | |
| // name: "complex camelCase", | |
| // input: "getUserXMLHTTPRequest", | |
| // expected: "get_user_xml_http_request", | |
| // }, | |
| } | |
| for _, cas := range testCases { | |
| t.Run(cas.name, func(t *testing.T) { | |
| result := ToSnakeCase(cas.input) | |
| if result != cas.expected { | |
| t.Errorf("ToSnakeCase(%q) got %q, BUT want %q", cas.input, result, cas.expected) | |
| } | |
| }) | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment