Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
157: Custom Flag Types
Most of the time, the built-in flags like Int, String, and Bool are plenty. But eventually, you'll hit a wall where you need something more complex. For example, I often find myself wanting to pass a list of tags or categories to a CLI tool—something like --tags=go,cli,tutorial. The standard flag package doesn't have a StringSlice type out of the box, so we have to build our own.
Defining the stringSlice type
To create a custom flag, we first need a type that knows how to handle the input. I'll start by defining a stringSlice as a slice of strings. This keeps the data organized and makes it easy to iterate over later in the program.
type stringSlice []string
Now, for this type to be compatible with the flag package, it must satisfy the flag.Value interface. This interface is dead simple: it just requires a String() method (to show the default value) and a Set(string) method (to parse the incoming command-line argument).
Implementing the Value interface
The String() method is mostly for the "help" menu. I'll just join the current slice elements with commas so the user sees a clean representation of the current state.
func (s *stringSlice) String() string {
return strings.Join(*s, ",")
}
The Set() method is where the actual work happens. This is where we take the raw string from the CLI and turn it into our slice. I'll use strings.Split to handle the comma-separated input.
func (s *stringSlice) Set(value string) error {
*s = strings.Split(value, ",")
return nil
}
Wiring it up (and the mistake I always make)
Now I want to actually use this in main(). Here is where I usually trip up. My instinct is to look for a helper function like flag.StringSlice(...), but those helpers only exist for the primitive types.
func main() {
var tags stringSlice
// This is where I'd instinctively try this:
// flag.StringSlice(&tags, "go", "Tags to filter by") // ERROR: This doesn't exist!
flag.Parse()
}
I forget that for any custom type implementing flag.Value, you have to use the generic flag.Var function. It's a bit less "sugar-coated" than the others, but it's the only way to hook in a custom type.
Let's fix that and put it all together:
package main
import (
"flag"
"fmt"
"strings"
)
type stringSlice []string
func (s *stringSlice) String() string {
return strings.Join(*s, ",")
}
func (s *stringSlice) Set(value string) error {
*s = strings.Split(value, ",")
return nil
}
func main() {
var tags stringSlice
flag.Var(&tags, "tags", "Comma-separated list of tags")
flag.Parse()
fmt.Printf("Filtering by tags: %v\n", tags)
}
Handling multiple flag declarations
One interesting detail about flag.Var is that if a user provides the flag multiple times (e.g., --tags=go --tags=cli), the Set method is called once for each occurrence.
In my current implementation, *s = strings.Split(value, ",") overwrites the slice every time. If you actually want to append the values instead of replacing them, you'd change that line to *s = append(*s, strings.Split(value, ",")...). Depending on your tool's UX, you might prefer one over the other. For this example, I'll stick with the replacement logic as it's more intuitive for comma-separated lists.
📋 Practical Task
Building a Coordinate Pair Flag
Your goal is to create a custom flag type called Coordinate that allows a user to pass a single flag representing an X and Y coordinate (e.g., --pos=10,20).
- Define a
Coordinatestruct withXandYas integers. - Implement the
flag.Valueinterface for*Coordinate. - In the
Set(string)method, split the input string by the comma and convert the resulting strings to integers usingstrconv.Atoi. - Return a helpful error if the input doesn't contain exactly two integers.
- Use
flag.Varinmain()to bind this type to the flag-pos. - Print the final X and Y values to the console.
There are no comments for now.