dt provides a stable, dependency-free foundation of common domain types for Go—types that are “good enough” for everyone to use, even if not perfect for every use-case.
Software ecosystems thrive when developers can build on shared assumptions instead of constantly reinventing them. dt aims to be that shared foundation: a stake in the ground for how common types like file paths, identifiers, and URLs that can be used in your packages with the knowledge that other packages can have access to those same types.
We’re not trying to design the ideal type for every situation. We’re trying to make it easy to agree on something usable so that code written by different teams and libraries can interoperate seamlessly.
When you write Go code using the standard library, you don’t have to ask whether string or os.FileInfo will be available to the next developer; they simply are. The goal of dt is to bring that same confidence and low-friction usability to domain types that the standard library never standardized.
This is pre-alpha and in development, thus subject to change—though I'm working toward v1.0 with confidence that the architecture is approaching stability. As of December 2025, I'm actively developing and using it in current projects.
If you find value in this project and want to use it, please open a discussion to let me know. If you discover any issues, please open an issue or submit a pull request.
dt uses explicit stability levels for exported symbols:
- Stable: no breaking changes to symbol identity or documented behavior
- Provisional: likely safe for production, but may still change as naming/shape matures
- Experimental: may change or disappear without notice
- Deprecated: scheduled for removal with a published removal floor
- Obsolete: retained for compatibility but should not be used
- Internal: exported for technical reasons but not part of the public API
Alignment note: OpenTelemetry provides useful prior art for maturity ladders and deprecation floors; this repo borrows vocabulary and minimum floors, but retains symbol-level enforcement goals that OTel does not target for Go API identifiers.
For Deprecated symbols:
- Minimum removal floor: 2 minor releases or 6 months, whichever is later
- Required metadata:
RemoveAfterandReplacement - We meet OTel's minimum and may exceed it; the symbol-level contract is authoritative
Contract blocks are machine-readable doc comments attached to exported identifiers.
// Filepath represents a filesystem path including a filename.
//
// Contract:
// - Stability: Stable
// - Since: v0.6.0
// - Note: Methods are stable and will not change without a major version.
type Filepath stringDeprecated items also include:
// - Deprecated: v0.8.0
// - RemoveAfter: v1.0.0 or 2026-01-01
// - Replacement: dt.NewFilepathPlanned tooling makes contracts enforceable:
- Extract
Contract:blocks into a generated JSON index - Validate that Deprecated items include
RemoveAfterandReplacement - Use
golang.org/x/exp/apidiffto detect breaking changes - Fail CI when Stable contracts break without a major version bump
Many Go developers recognize that custom domain types can improve correctness and readability. Yet few actually use them, because in today’s ecosystem doing so requires too much effort.
The barrier is friction. Even simple types like Filename or DirPath often demand re-implementing helper methods, sacrificing interoperability with third-party libraries, and working around the standard library’s limited type flexibility.
dt envisions a future where that friction is greatly reduced.
"When different packages from different authors both use
dt.Filepathordt.DirPath, they can exchange values directly. No glue code, no type conversions, and greatly reduced risk of mismatched assumptions."
— Thedtteam
Today, developers who use custom types end up isolated. A package defining its own FilePath or DirPath cannot interoperate cleanly with others that do the same. Every ecosystem needs a shared vocabulary.
For Go, we hope that dt can be that vocabulary.
Using domain types in Go shouldn’t feel like swimming upstream.
Because the Go standard library works almost exclusively with built-in types (string, int, etc.), developers are discouraged from introducing semantic wrappers—even when they make code safer and more expressive. dt solves this by doing the hard part once, in one place, and promising to keep it stable.
We want to make it easy and obvious to choose dt.Filename instead of string, and to know that everyone else is doing the same.
A community-driven, well-designed set of types that:
- 🧱 Provide semantic clarity for common domains (files, identifiers, URLs, etc.).
- ⚙️ Work seamlessly with the Go standard library and third-party packages.
- 🔄 Enable interoperability across projects and teams.
- 🧭 Are stable and intuitive, with minimal learning curve.
- 🌍 Encourage a shared ecosystem vocabulary.
Our reasonable goal is broad adoption: if enough developers and library authors agree to depend on dt, interoperability naturally follows.
Our ultimate goal — said with a wink — is to see the Go team adopt something like dt officially, perhaps as golang.org/x/dt. Whether or not that ever happens, the mission remains the same: to make domain types first-class citizens in Go.
As Rob Pike noted:
"Gofmt's style is no one's favorite, yet gofmt is everyone's favorite."
We believe the same principle could (should?) apply to this Domain Types package:
"Types in Domain Types are no one's favorite, yet the Types in Domain Types are everyone's favorite."
Just as gofmt succeeds by being good enough rather than perfect, dt succeeds by establishing a shared vocabulary that everyone can build on. It's not about designing the ideal type for every situation—it's about making it easy to agree on something usable, so developers and libraries can interoperate seamlessly without constant friction.
- 🧩 Simplicity: Each type does one thing and does it well.
- 🛠️ Minimal Logic: No complex behavior—just useful helpers like
ReadFile()orEnsureExists()that feel native to Go. - 🧭 Stability: After
v1.0.0, no breaking changes without a major bump. - 🕰️ Longevity: Designed to remain relevant for many years without redesign.
- 🧑🤝🧑 Interoperability: Always safe to use in your own packages and libraries.
If you're coming from the Go standard library, you might notice that dt frequently uses methods on types rather than package-level functions:
// Standard library typically uses package functions
var fp string
...
content, err := os.ReadFile(fp)
dir := filepath.Dir(dp)
// dt typically uses methods
var fp dt.Filepath
...
content, err := fp.ReadFile()
dir := fp.Dir()This is not a stylistic preference. It's a pragmatic response to a technical limitation.
Go's generics system (as of Go 1.25) simply cannot express the type relationships needed to make package functions work ergonomically with domain types. Defaulting to package functions in dt would force constant type casting, defeating the impetus behind dt, to provide greater type safety that string and other built-in types provide without having to constantly cast values from one derived type to another type derived from the same base type:
// A package function approach would require verbose casting
var myPath dt.Filepath
var myDir dt.DirPath
...
myDir = dt.DirPath(filepath.Dir(string(myPath))
// Whereas a method approach is clean and type-safe
var myPath dt.Filepath
var myDir dt.DirPath
...
myDir = myPath.Dir()The method-based approach provides:
- Type-casting rarely required – operations are attached to the type for better ergonomics
- Better discoverability – IDE autocompletes will shows all available operations
- Cleaner composition – Chaining for those who prefer it, e.g.:
dir.Join(file).ReadFile() - And best of all: Type safety – The Go compiler prevents invalid type compositions
For a detailed technical explanation of this decision, see ADR 2025-02-11: Methods Over Package Functions.
dt provides two categories of types:
Domain types that model real-world concepts and are intended to be used as replacements for built-in types like string. These represent semantic entities: file paths, identifiers, URLs, versions, and the like.
Examples: Filename, Filepath, DirPath, URL, Identifier, Version
Types that provide essential functionality for core types—either as containers or enumerations. These are used alongside core types to enable safe operations and type-safe classification.
Examples: DirEntry (container), EntryStatus (enumeration)
The following diagram shows how dt types relate to one another, from broad to narrow:
Types
├── Supplemental Types
│ ├── DirEntry (used during directory walking)
│ └── EntryStatus (enumeration: FileEntry, DirEntry, SymlinkEntry, etc.)
└── Core Types (all extend string)
├── Identifier (validated identifiers)
├── TimeFormat (time layout strings)
├── Version (software version strings)
├── EntryPath (generic file or directory path)
│ ├── DirPath (directory path)
│ │ ├── TildeDirPath (tilde-prefixed directory path)
│ │ ├── VolumeName (Windows volume identifier)
│ │ └── PathSegments
│ │ └── PathSegment (single path component)
│ └── Filepath (file path with filename)
│ ├── RelFilepath (relative file path with traversal protection)
│ │ └── Filename (filename without path)
│ │ └── FileExt (file extension with leading period)
│ └── Filename (filename without path)
│ └── FileExt (file extension with leading period)
├── RelPath (generic relative file or directory path)
│ ├── PathSegments
│ │ └── PathSegment (single path component)
│ └── RelFilepath (relative file path with traversal protection)
│ └── Filename (filename without path)
│ └── FileExt (file extension with leading period)
├── InternetDomain (internet domain names)
└── URL (syntactically valid URLs)
├── URLSegments (URL path components)
│ └── URLSegment (single URL path component)
└── Filename (when used in URL contexts)
└── FileExt
Many types in dt have associated Parse<Type>() functions:
filename, err := dt.ParseFilename("config.json")
url, err := dt.ParseURL("https://example.com")
version, err := dt.ParseVersion("1.2.3")Current Status: These functions are currently lightweight casting functions. Over time, they will evolve to include robust validation similar to url.Parse() from the standard library.
Future Intent: As validation is implemented progressively, the type hierarchy understanding will evolve to reflect validated constraints. This design ensures dt can add validation without breaking existing code.
Throughout the dt API, the term "file" specifically means regular file, as determined by fs.FileMode.IsRegular(). This aligns with Go standard library conventions.
A regular file is a normal data file—the most common file system entry type. It excludes:
- Directories
- Symbolic links
- Named pipes (FIFOs)
- Unix sockets
- Device files (block or character)
- Other irregular entries
Methods and functions with "File" in their name operate on regular files only:
IsFile()— Returnstrueonly for regular filesWalkFiles()— Yields only regular filesFilename()— Available for regular files
To process all entry types including symlinks and special files, use:
Walk()— Yields all entry typesStatus()— Classify entry type- Pattern matching on
EntryStatusvalues
func processAllEntries(root dt.DirPath) (err error) {
for entry := range root.Walk() {
var status dt.EntryStatus
status = entry.Status()
switch status {
case dt.IsFileEntry:
// Regular file
fmt.Println("File:", entry.Filename())
case dt.IsDirEntry:
// Directory
fmt.Println("Dir:", entry.DirPath())
case dt.IsSymlinkEntry:
// Symbolic link
var target dt.EntryPath
var path dt.EntryPath
path = entry.EntryPath()
target, err = path.Readlink()
if err != nil {
goto end
}
fmt.Println("Symlink:", path, "->", target)
case dt.IsSocketEntry, dt.IsPipeEntry, dt.IsDeviceEntry:
// Special file types
fmt.Println("Special:", entry.EntryPath())
default:
// Unknown or error
fmt.Println("Other:", entry.EntryPath())
}
}
end:
return err
}This distinction ensures type safety and predictable behavior across the API.
Filesystem paths are the core focus of dt. They represent locations on the filesystem and provide type-safe operations for reading, writing, and navigation.
Represents a filesystem directory path (absolute or relative).
Key Methods:
EnsureExists()— Create directory and parents if needed; error if path exists as fileReadDir()— List directory contentsWalk()— Iterate through directory tree withSkipDir()supportWalkFiles()— Iterate through regular files onlyWalkDirs()— Iterate through directories onlyJoin(...any)— Join path componentsDir()— Parent directoryBase()— Directory name asPathSegmentClean()— Normalize pathStat()— Get file info (follows symlinks)Lstat()— Get file info without following symlinksExists()— Check existenceDirFS()— Convert tofs.FS
Comprehensive Example:
func processDirPath() (err error) {
var dir dt.DirPath
var subDir dt.DirPath
var configFile dt.Filepath
dir = dt.DirPath("/home/user/projects")
err = dir.EnsureExists()
if err != nil {
goto end
}
// Walk all regular files recursively
for entry := range dir.WalkFiles() {
fmt.Println("File:", entry.Filename())
}
// Create and navigate subdirectory
subDir = dir.Join("src", "main")
err = subDir.EnsureExists()
if err != nil {
goto end
}
// Work with files in subdirectory
configFile = dt.FilepathJoin(subDir, "config.json")
err = configFile.WriteFile([]byte("{}"), 0o644)
if err != nil {
goto end
}
end:
return err
}Directory path with tilde (~) prefix for user home directory expansion.
Key Methods:
Expand()— Expand tilde to full path
Package Function:
ParseTildeDirPath(s string)— Parse string as tilde directory path
Example:
func processHomePath() (dir dt.DirPath, err error) {
var tildePath dt.TildeDirPath
tildePath, err = dt.ParseTildeDirPath("~/projects/go-dt")
if err != nil {
goto end
}
dir, err = tildePath.Expand()
if err != nil {
goto end
}
fmt.Println("Expanded path:", dir)
end:
return dir, err
}Note: Expand() is not strict about tilde path validity. It will expand any path, including non-tilde paths like ".", by resolving them relative to the user's home directory. It only returns an error if the underlying os.UserHomeDir() call fails.
Represents a complete file path including filename and extension.
Key Methods:
ReadFile()— Read file contentsWriteFile(data, mode)— Write fileCreate()— Create fileOpenFile(flag, mode)— Open with flagsDir()— Parent directory asDirPathBase()— Filename asFilenameExt()— File extension asFileExtStat(),Lstat()— File infoExists()— Check existenceCopyTo(dest, opts)— Copy file to destination with optional settingsCopyToDir(dest, opts)— Copy file to destination directoryRemove()— Delete file
Comprehensive Example:
func processFile() (data []byte, err error) {
var file dt.Filepath
var dir dt.DirPath
file = dt.Filepath("/home/user/config.json")
// Check and read file
if ok, statErr := file.Exists(); !ok {
err = statErr
goto end
}
data, err = file.ReadFile()
if err != nil {
goto end
}
// Get parent directory
dir = file.Dir()
fmt.Println("Config location:", dir)
// Copy to backup
backup := dt.FilepathJoin(dir, "config.json.bak")
err = file.CopyTo(backup, nil)
if err != nil {
goto end
}
end:
return data, err
}Represents a relative file path with protections against directory traversal attacks. Validates that the path does not attempt to escape the intended directory using ../ sequences.
Key Methods:
Dir()— Parent directoryBase()— FilenameValidPath()— Check path validityStat(fileSys ...fs.FS)— Get file infoReadFile(fileSys ...fs.FS)— Read file contentsWriteFile(data, mode)— Write fileRel(baseDir)— Get relative pathExists()— Check existenceStatus(flags)— Get entry status
Note: RelFilepath currently has fewer methods than Filepath. This is not a fundamental design constraint—additional methods will be added as use cases arise. The limited method set reflects practical usage patterns rather than architectural limitations.
Example:
func processUserFile(userPath string) (data []byte, err error) {
var relPath dt.RelFilepath
var valid bool
relPath = dt.RelFilepath(userPath)
valid = relPath.ValidPath()
if !valid {
err = errors.New("invalid relative path")
goto end
}
data, err = relPath.ReadFile()
if err != nil {
goto end
}
end:
return data, err
}Generic relative path that can represent either a file or directory. Similar to EntryPath but constrained to relative paths.
Key Methods:
Dir()— Parent directoryBase()— Base name asPathSegmentStat(fileSys ...fs.FS)— Get file infoLstat()— Get file info without following symlinksStatus(flags)— Get entry statusReadlink()— Resolve symlink targetVolumeName()— Get volume nameAbs()— Convert to absolute pathJoin(elems ...any)— Join path componentsHasSuffix(suffix),Contains(substr)— String operations
Example:
func processRelativePath(rel dt.RelPath) (err error) {
var abs dt.RelPath
var status dt.EntryStatus
abs, err = rel.Abs()
if err != nil {
goto end
}
status, err = abs.Status()
if err != nil {
goto end
}
fmt.Printf("Path: %s, Type: %s\n", abs, status)
end:
return err
}Generic filesystem entry path that can represent either a file or directory. Use when the type is unknown until runtime.
Key Methods:
Dir()— Parent directoryBase()— Base name asPathSegmentStat(fileSys ...fs.FS)— Get file infoLstat()— Get file info without following symlinksStatus(flags ...EntryStatusFlags)— Get entry type classificationReadlink()— Resolve symlink targetVolumeName()— Get volume name (Windows)Abs()— Get absolute pathJoin(elems ...any)— Join path componentsHasSuffix(suffix),Contains(substr)— String operationsEnsureTrailSep()— Ensure trailing separatorHasDotDotPrefix()— Check for..prefix
Type Checking Pattern:
func processEntry(ep dt.EntryPath) (err error) {
var status dt.EntryStatus
status, err = ep.Status()
if err != nil {
goto end
}
switch status {
case dt.IsFileEntry:
fp := dt.Filepath(ep)
// Handle regular file
case dt.IsDirEntry:
dp := dt.DirPath(ep)
// Handle directory
case dt.IsSymlinkEntry:
var target dt.EntryPath
target, err = ep.Readlink()
if err != nil {
goto end
}
// Handle symlink
case dt.IsSocketEntry, dt.IsPipeEntry, dt.IsDeviceEntry:
// Handle or skip special types
}
end:
return err
}Note: EntryPath does not have IsFile() or IsDir() methods. Use Status() for type classification and cast to the appropriate type.
Filename without any path component.
Key Methods:
Ext()— Extension asFileExtString()— Get filename as string
Example:
fn := dt.Filename("document.txt")
ext := fn.Ext() // FileExt(".txt")
name := fn.String() // "document.txt"File extension including the leading period.
Type: FileExt is a bare string type with no public methods.
Usage:
ext := dt.FileExt(".txt")
name := string(ext) // Convert to string when neededNote: FileExt currently has no methods. Extension validation and manipulation can be done through standard string operations after casting.
Working with components of filesystem paths.
Represents a filesystem path as a string with segment operations.
Key Methods:
Split()— Split into[]PathSegmentusing OS separatorSegment(index)— Get segment at indexSlice(start, end)— Get segment slice (supportsend == -1for "to last")SliceScalar(start, end)— Get joined substring of segments without intermediate allocationsLastIndex(sep)— Find last occurrence of separator
Example:
path := dt.PathSegments("home/user/projects/file.go")
segments := path.Split() // []PathSegment{"home", "user", ...}
segment := path.Segment(1) // "user"
slice := path.Slice(0, 3) // first 3 segments
scalar := path.SliceScalar(0, 3) // "home/user/projects"A single filesystem path component.
Key Methods:
HasPrefix(prefix),HasSuffix(suffix)— String prefix/suffix checksTrimPrefix(prefix),TrimSuffix(suffix)— Remove prefix/suffixContains(substr)— Check substring presence
Working with URLs and URL components.
Represents a syntactically valid Uniform Resource Locator.
Key Methods:
Parse()— Parse into*url.URLfor detailed accessGET(client)— Perform HTTP GET requestHTTPGet(client)— Perform HTTP GET request (alias)
Comprehensive Example:
func fetchData(endpoint string) (body []byte, err error) {
var apiURL dt.URL
var resp *http.Response
apiURL, err = dt.ParseURL(endpoint)
if err != nil {
goto end
}
resp, err = apiURL.GET(http.DefaultClient)
if err != nil {
goto end
}
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
goto end
}
end:
return body, err
}Represents URL path segments (parts separated by /).
Key Methods:
Split()— Split into[]URLSegmentSegment(index)— Get segment at indexSlice(start, end)— Get segment sliceSliceScalar(start, end, sep)— Get joined scalar with custom separatorLastIndex(sep)— Find last occurrence of separatorBase()— Last segment asURLSegment
Example:
segments := dt.URLSegments("api/v1/users/123")
all := segments.Split() // []URLSegment{"api", "v1", "users", "123"}
middle := segments.Slice(1, 3) // []URLSegment{"v1", "users"}
scalar := segments.SliceScalar(1, 3, "/") // "v1/users"
last := segments.Base() // "123"A single URL path component, semantically different from a PathSegment.
A string type representing validated identifiers suitable for Git references, semantic version components, and similar uses.
Example:
ref := dt.Identifier("main")
tag := dt.Identifier("v1.2.3")Software version string following semantic versioning conventions.
Key Methods:
Major(),Minor(),Patch()— Extract version componentsValid()— Check if version is valid
Internet domain name (e.g., example.com).
Key Methods:
Valid()— Validate domain formatTLD()— Extract top-level domainString()— Get as string
Time layout string for use with time.Parse() and time.Format().
Mounted volume name, primarily for Windows support (e.g., C:).
Represents a filesystem entry encountered during directory walking.
Key Methods:
Name()— Entry name asPathSegmentFilename()— Filename asFilename(if file)DirPath()— Full path asDirPathFilepath()— Full path asFilepath(if file)Status()— Entry type asEntryStatusIsFile(),IsDir(),IsSymlink()— Type checksInfo()— Get underlyingfs.FileInfo
Enumeration classifying filesystem entries:
FileEntry— Regular fileDirEntry— DirectorySymlinkEntry— Symbolic linkUnknownEntry— Other or unknown type
These functions work with any string type and separator, providing zero-cost abstractions for delimited string manipulation.
func SplitSegments[S ~string](s, sep string) []SSplits a string by separator into typed segments. Pre-counts separators for optimal memory allocation.
Example:
func example() {
segments := dt.SplitSegments[dt.PathSegment]("home/user/projects", "/")
// segments = []PathSegment{"home", "user", "projects"}
}func IndexSegments[S ~string](s, sep string, index int) SReturns the segment at the given index. Returns empty string if index is out of bounds or negative.
Example:
func example() {
segment := dt.IndexSegments[dt.PathSegment]("home/user/projects", "/", 1)
// segment = "user"
}func SliceSegments[S ~string](s, sep string, start, end int) []SReturns a slice of segments from start (inclusive) to end (exclusive). Supports end == -1 to mean "to the last segment". Returns empty slice if indices are invalid.
Example:
func example() {
segments := dt.SliceSegments[dt.PathSegment]("home/user/projects/file.go", "/", 1, 3)
// segments = []PathSegment{"user", "projects"}
}func SliceSegmentsScalar[S ~string](s, sep string, start, end int) SLike SliceSegments, but returns a joined scalar string instead of a slice. Zero heap allocations — uses single-pass byte position tracking, ideal for extracting contiguous segment ranges.
Performance Note: This function is optimized for memory efficiency with exactly one pass through the input string without creating intermediate data structures.
Example:
func example() {
result := dt.SliceSegmentsScalar[dt.PathSegment]("home/user/projects/file.go", "/", 1, 3)
// result = "user/projects" (no intermediate allocations)
}func JoinSegments[S ~string](ss []S, sep string) SJoins a slice of segments with a separator. Pre-calculates required capacity for minimal allocations.
Example:
func example() {
segments := []dt.PathSegment{"home", "user", "projects"}
result := dt.JoinSegments(segments, "/")
// result = "home/user/projects"
}dt provides generic join functions for safely combining path and URL components with type preservation. These functions use generics to accept any string-like types.
// Generic join functions for any string-like types
func DirPathJoin[T1, T2 ~string](a T1, b T2) DirPath
func DirPathJoin3[T1, T2, T3 ~string](a T1, b T2, c T3) DirPath
func DirPathJoin4[T1, T2, T3, T4 ~string](a T1, b T2, c T3, d T4) DirPath
func DirPathJoin5[T1, T2, T3, T4, T5 ~string](a T1, b T2, c T3, d T4, e T5) DirPath
func FilepathJoin[T1, T2 ~string](a T1, b T2) Filepath
func FilepathJoin3[T1, T2, T3 ~string](a T1, b T2, c T3) Filepath
func FilepathJoin4[T1, T2, T3, T4 ~string](a T1, b T2, c T3, d T4) Filepath
func FilepathJoin5[T1, T2, T3, T4, T5 ~string](a T1, b T2, c T3, d T4, e T5) Filepath
func RelFilepathJoin[T1, T2 ~string](a T1, b T2) RelFilepath
func RelFilepathJoin3[T1, T2, T3 ~string](a T1, b T2, c T3) RelFilepath
func RelFilepathJoin4[T1, T2, T3, T4 ~string](a T1, b T2, c T3, d T4) RelFilepath
func RelFilepathJoin5[T1, T2, T3, T4, T5 ~string](a T1, b T2, c T3, d T4, e T5) RelFilepath
func EntryPathJoin[T1, T2 ~string](a T1, b T2) EntryPath
func EntryPathJoin3[T1, T2, T3 ~string](a T1, b T2, c T3) EntryPath
func EntryPathJoin4[T1, T2, T3, T4 ~string](a T1, b T2, c T3, d T4) EntryPath
func EntryPathJoin5[T1, T2, T3, T4, T5 ~string](a T1, b T2, c T3, d T4, e T5) EntryPathfunc PathSegmentsJoin[T1, T2 ~string](a T1, b T2) PathSegments
func PathSegmentsJoin3[T1, T2, T3 ~string](a T1, b T2, c T3) PathSegmentsfunc URLJoin[T1, T2 ~string](a T1, b T2) URL
func URLJoin3[T1, T2, T3 ~string](a T1, b T2, c T3) URL
func URLJoin4[T1, T2, T3, T4 ~string](a T1, b T2, c T3, d T4) URL
func URLJoin5[T1, T2, T3, T4, T5 ~string](a T1, b T2, c T3, d T4, e T5) URL
func URLSegmentsJoin[T1, T2 ~string](a T1, b T2) URLSegments
func URLSegmentsJoin3[T1, T2, T3 ~string](a T1, b T2, c T3) URLSegmentsExample:
func buildPaths() {
baseDir := dt.DirPath("/home/user")
// Join accepts any string-like types
subDir := dt.DirPathJoin(baseDir, "projects")
configFile := dt.FilepathJoin(baseDir, "config.json")
// Fixed-arity versions for multiple components
logFile := dt.FilepathJoin3(baseDir, "logs", "app.log")
}dt wraps common os package functions with type safety.
func MkdirAll(path dt.DirPath, perm os.FileMode) error
func MkdirTemp(dir dt.DirPath, pattern string) (dt.DirPath, error)
func RemoveAll(path dt.DirPath) errorfunc CreateFile(path dt.Filepath) (*os.File, error)
func CreateTemp(dir dt.DirPath, pattern string) (*os.File, error)
func ReadFile(path dt.Filepath) ([]byte, error)
func WriteFile(path dt.Filepath, data []byte, perm os.FileMode) errorfunc Getwd() (dt.DirPath, error)
func TempDir() dt.DirPath
func UserHomeDir() (dt.DirPath, error)
func UserConfigDir() (dt.DirPath, error)
func UserCacheDir() (dt.DirPath, error)func DirFS(path dt.DirPath) fs.FSExample:
func setupApplicationDirs() (err error) {
var homeDir dt.DirPath
var configDir dt.DirPath
homeDir, err = dt.UserHomeDir()
if err != nil {
goto end
}
configDir = dt.DirPathJoin(homeDir, ".config", "myapp")
err = dt.MkdirAll(configDir, 0o755)
if err != nil {
goto end
}
tempDir, err := dt.MkdirTemp(dt.TempDir(), "myapp-*")
if err != nil {
goto end
}
defer dt.RemoveAll(tempDir)
end:
return err
}dt uses the structured error system from the doterr package internally. We recommend you use doterr as well for consistent error handling throughout your applications.
Reference: See go-doterr for complete documentation on structured error metadata and chaining.
func CanWrite(path dt.Filepath) (bool, error)
func Stat(path dt.EntryPath) (os.FileInfo, error)
func StatFile(path dt.Filepath) (os.FileInfo, error)
func StatDir(path dt.DirPath) (os.FileInfo, error)func Chtimes(path dt.EntryPath, atime, mtime time.Time) error
func ChangeFileTimes(path dt.Filepath, atime, mtime time.Time) error
func ChangeDirTimes(path dt.DirPath, atime, mtime time.Time) errorfunc ParseTimeDurationEx(duration string) (time.Duration, error)func Logger() *slog.Logger
func SetLogger(logger *slog.Logger)
func EnsureLogger()
func LogOnError(err error)
func CloseOrLog(closer io.Closer)Example:
func checkFileAccess(filePath dt.Filepath) (err error) {
var info os.FileInfo
var canWrite bool
canWrite, err = dt.CanWrite(filePath)
if err != nil {
goto end
}
info, err = dt.StatFile(filePath)
if err != nil {
goto end
}
fmt.Printf("File: %s, Writable: %v, Size: %d\n",
filePath, canWrite, info.Size())
end:
return err
}Experimental types and utilities under evaluation for potential inclusion in the main dt package. Safe for production use with strong compatibility guarantees, though subject to evolution when necessary.
Key Features:
GetWorkingDir()— OS-aware working directory detection with hint supportIsZero(),IsNil(),IsNilable(),IsNilableKind()— Type introspection helpersMust()— Panic on error helper for fail-fast patternsPanicf()— Formatted panic functionAssertType()— Safe type assertion with panic fallbackTempTestDir(),SetTestEnv()— Testing and environment helpers- OS-specific path segment parsers (Windows, Darwin, Linux)
EntryStatusError()— ConvertEntryStatusto error types
Package: go-dt/dtx
Pattern-based file operations with glob-style matching and bulk copy operations.
Key Features:
Glob— Type-safe glob pattern representationGlobRule— Single file copy operation specificationGlobRules— Container for multiple rules with batchCopyTo()operation
Package: go-dt/dtglob (if available in your installation)
Standard interface for describing application metadata across the ecosystem.
Key Features:
AppInfointerface — Contract for application metadata including name, version, config pathsNew(Args)— Create concreteAppInfoimplementations- Test helpers for verifying
AppInfoimplementations
Package: go-dt/appinfo
| Short answer | Shared types reduce friction and make codebases interoperate. |
| Longer answer | When two packages both use a domain type like dt.DirPath they exchange values without constant type casting.Using a 3rd package eliminates dreaded import cycles. Using dt saves time, reduces bugs, and makes APIs easier to compose across teams. |
| Minimal adoption | Use a single type (for example dt.Filepath) at package boundaries. |
| Compatibility angle | Stable shared types are a stronger guarantee than ad-hoc local types that can drift or be renamed. |
| Short answer | You lose the key benefit of domains types: interoperability. |
| Longer answer | Local types fracture the ecosystem because each package invents its own names and helpers. Local types require constant type casting across packages. dt aims to be the stable, no (other) dependency. shared vocabulary. You often must create your own 3rd package to avoid import cycles anyway, why not use dt instead? |
| Minimal adoption | Use dt types at package boundaries, keep internals local. |
| Compatibility angle | Shared stable types reduce breakage at integration points. |
| Short answer | Small libraries benefit most from stable compatibility signals. |
| Longer answer | Small teams often ship faster and have fewer release resources, so breaking changes are more costly. A small set of stable types reduces downstream churn. |
| Minimal adoption | Use only 1-2 core types and ignore the rest. |
| Compatibility angle | A tiny stable surface is easier to preserve than a large, implicit one. |
| Short answer | Types encode intent and reduce mistakes. |
| Longer answer | string does not tell readers or tooling whether the value is a filename, a URL, or an identifier.dt types make intent explicit and enable safe helper methods.Validating Parse<type>()(<type>,error) funcs for each type make baked-in assumptions explicit and actionable. |
| Minimal adoption | Replace only the most error-prone string fields in public APIs. |
| Compatibility angle | Stable types keep public contracts clear even as internals change. |
| Short answer | Use a small set and let tooling teach the rest. |
| Longer answer | A handful of domain types is easy and obvious to learn. Method-based API are discoverable in IDEs. Linters can enforce conventions at the boundaries. |
| Minimal adoption | Use only the types that already map to your domain language. Start with dt.DirPath and dt.Filepath, for example. |
| Compatibility angle | Clear, stable types reduce downstream friction and support burden. |
dt is very open to collaboration; we are actively seeking it. Our intention is to recruit enough active contributors that governance can eventually move to a dedicated GitHub organization. The aim is a community-led defacto-standard that remains practical, stable, and inclusive.
If you share this vision — whether as a library author, contributor, or just a developer who’s tired of having to use a string type instead of a bespoke domain type becausthe friction is just too great — start or join a discussion and/or submit a pull requestto help drive what dt can become.
MIT License — see LICENSE for details.