Skip to content
Derk Norton edited this page Jun 4, 2024 · 40 revisions

TL;DR

Go Module

Overview

This document defines the coding conventions and idioms used in all Crater Dog Technologies™ (aka "craterdog") Go modules. It also describes how to use an associated code generator to generate the concrete class implementations for all abstract class interfaces defined in a Package.go file.

⚠️ This alert symbol is used throughout the document to highlight novel concepts and to point out places where our coding conventions differ from the conventions recommended by the Go development team. A reader who is very familiar with the Go language may want to just skim the parts of the document that are not highlighted in this manner.

The Go programming language is fairly simple and runs efficiently on most platforms. But it is a low-level language like C—so we define an additional higher-level class-based model on top of the basic Go type, interface, method and function language constructs to make it easier to write and maintain good Go code:

  • An aspect interface defines a cohesive subset of the methods associated with a single aspect of one or more classes.
  • A class interface defines the set of methods that allow access to all class-level constants, constructors and functions so that there wont be any name collisions between classes defined within the same package.
  • An instance interface defines the complete set of methods that allow each instance of a class to be accessed and managed separately.
  • A concrete class provides an implementation of each method defined in its associated class interface and instance interface—which may in turn utilize multiple aspect interfaces.

The class-based model is used extensively throughout each section of this coding conventions document:

Words to Live By

To set the stage, we share some maxims that we think are valuable when it comes to writing good, secure, maintainable code.

"Say what you mean, and mean what you say."

Since code clarity is important—we name each major Go concept consistently using a different part of speech:

  • Type and constant names are always noun phrases that identify something.
  • Interface names are always adjective phrases that describe something.
  • Method and function names are (almost)* always verb phrases that do something.

The Go naming guidelines are less strict than this but the added clarity of our naming conventions makes it worth the effort.

"Avoid global variables—anyone can mess with them at any time."

This one seems obvious—just use global constants instead, right? After all, global constants are generally safe, both from a security perspective and from a threading perspective. Unfortunately, the Go language only allows primitive Go data types (e.g. int, float64, string, etc.) to be marked as constant. We need to be able to make a reference to any type of value immutable.

⚠️ Fortunately, there is a proven work-around for this problem. We can do the following:

  1. Define a private class variable that holds the desired constant value.
  2. Define a public class method that returns a reference to the value.

Calling a method to retrieve a constant is therefore safe and secure—but still very efficient—since the compiler should optimize the code by directly returning a reference to the private variable.

* Note that a method that returns the value of a constant is given the name of the constant—which is a noun phrase rather than a verb phrase. Also, some functions like Average, Xor, etc., that return the result of a calculation are given the name of the calculation—which is also a noun phrase.

When designing your code:

"Aim for a Goldie Locks level of abstraction."

At the beginning of a coding project we often bounce around between really low-level abstractions and really high-level ones. Over time we settle on something in between. Finding that middle ground takes practice—and even then—we must still look at all of our abstractions to make sure they are all at about the same level.

A good example of mismatched levels of abstraction is using a string to represent the mailing address as part of a customer class. All mailing addresses are strings but not all strings are mailing addresses.

Another famous example comes from the collection classes defined in the original version of the Java programming language. They claimed that a stack was a vector even though the two concepts have completely different interfaces and are really two different levels of abstraction.

A list and a stack would both be at about the same level of abstraction, but again a list is not a stack and a stack is not a list. They are two separate collection abstractions. The confusion between interface inheritance and implementation inheritance is likely the main reason that object-oriented programming has fallen out of favor.

And probably the most important maxim around writing good code is the following:

"Concrete classes should only depend on abstract definitions, not the other way around."

⚠️ This cannot be overstated. When nothing depends on a concrete class we are free to change its implementation without affecting any other code. As long as the new implementation still provides the semantics defined by its abstract interfaces, none of the other code modules that use the concrete class should break.

Obviously, at some point things need to depend on primitive Go data types. But these do qualify as abstract definitions since the Go language specification nails down their exact semantics and is—generally—not allowed to change those semantics.

To some extent, the Go language also helps enforce this maxim by not allowing circular dependencies between packages or interfaces. It is still all too easy to define an abstraction that depends on a concrete class. So we must be diligent in carefully checking our package abstractions for any dependencies on concrete classes.

Finally:

"Don't spend time writing code that can be automatically generated."

An awful lot of code is really boilerplate code that we spend time copying from somewhere and pasting into our own code. Many copy-paste errors occur during this process. It is better to generate as much code as possible based on an abstract description of the classes we need in our package—combined with code templates that meet our coding standards.

A Class-Based Approach

These maxims—along with years of experience—have led us to define higher-level concepts that follow a more traditional, non-object-oriented, class-based approach to Go code development.

⚠️ The higher-level concepts are organized within a Go module as follows:

  • Package.go - This file defines and exports—for each package in a module—the following abstractions for use by other Go code modules:
    • Simple Go type and const definitions that require no additional implementation details.
    • Go interface definitions for the class methods, instance methods and aspect methods—that may use the above simple type definitions—and are implemented by the concrete classes provided by the implemented package.
    • Each definition in this file should be accompanied by a go doc style comment describing the defined concept from the client's perspective.
  • <class>.go - Each class file provides implementations for the following—which support a subset of the interfaces exported by the abstract class model defined in the Package.go file:
    • A class function that returns a reference to the class which can be used to access class-level constants, methods and function defined in the class interface.
    • A set of class-level methods that implement the class interface defined in the Package.go file for the class.
    • A set of instance-level methods that implement the instance interface defined in the Package.go file and control access to the private attributes maintained by each instance of the class.
    • Class implementation files should be self-documenting and never contain go doc style comments. Internal (// style) comments should be limited to places where something important must be conveyed to the developer. Comments highlighting steps within a method should be replaced by private method calls to methods that implement each step.

Here is what the directory structure might look like for a collections package:

collections/
	Package.go
	catalog.go
	list.go
	queue.go
	set.go
	stack.go

There is a single Package.go class model file followed by a <class>.go file for each concrete collection class defined in the package. The name of the class model file is capitalized so that it appears first in a directory listing.

⚠️ The Go language supports only very weak inheritance and polymorphism, and since—as mentioned above—many developers abuse object-oriented programming by using implementation inheritance, we opted not to include inheritance—of any kind—in our class-based model. Instead, we rely on delegation through interfaces to handle the cases where a developer might be tempted to use inheritance. Some people refer to this model as an "object-based model" but since our model is centered around the concept of classes we prefer to still call it a "class-based model".

Code Templates and Examples

Much of this document describes how to implement these higher-level concepts by providing examples of each concept along with links to code templates that make it easy to insert them into our code modules. We will repeatedly use three example classes of increasing complexity to demonstrate each concept:

  • Angle - A concrete class that extends the primitive float64 Go data type.
  • Association - A generic concrete class that encapsulates a key-value pair of attributes.
  • Catalog - A generic concrete class that encapsulates an ordered Go map-like catalog of key-value pairs.

Each of the above class names is a link to the Go playground code implementing the example class to be viewed and run.

We now take a look—in much more detail—at each concept that makes up our class-based programming model—starting with the Go modules themselves.

Modules

A module is the most coarse grained entity in Go. It is maintained as a unit in a source code repository like https://github.com.

Module Names

The module name will be the same as the name of the project in the source code repository. We prefix each module name with the programming language used in the project, for example: go-collection-framework. There is no real benefit to using terse module names so make sure the name is informative.

Version Numbers

Each module may consist of multiple versions (i.e. "v2.0.0", "v2.3.1", "v3.1.0" etc.) of the packages it contains.

⚠️ The versioning of modules written for the Go language is—more than a little—confusing. The files for the "first" version of a module must be in the module's root directory. Those files will either be considered version "v1" or "v0" depending on whether or not the release is stable.

For versions of modules "v2.0.0" and greater, a separate subdirectory for each major version (e.g. v2/) is required under the module's root directory. The result is that we end up with obsolete files in the root directory, with the latest versions in their own subdirectories. Someone looking at the project for the first time will see the obsolete files first—which make no sense at all.

This amount of clutter in the project root directory is not acceptable. The folks at Google™ must—at this point—be rather 😔 with themselves by this design. It would have made some sense if "v1" could have its own subdirectory and the "v0" files removed after "v1" was released but alas...

Nevertheless, we must work around this problem. We have chosen to consider the first version (v1)—residing in the root directory of a module—experimental and never release it. Then—once the module is deemed ready for release—the code is moved into a new v2/ directory and the module is released as version "v2.0.0". This provides a consistent directory structure across all versions and—at the same time—eliminates the clutter in the root directory. So a typical project directory structure might look like this:

go-example-module/
	LICENSE
	README.md
	v2/
	v3/
	v4/

Nice and clean!

Packages

A Go module contains one or more packages with each package residing in its own directory. The code in a Go module depends on code from other Go packages directly rather than indirectly via other Go modules. In other words, the package name is included in the dependency rather than just the module name. These dependencies are specified using the Go import statement, for example:

import (
	col "github.com/craterdog/go-collection-framework/v4/collection"
)

In this example, the module name is "github.com/craterdog/go-collection-framework/v4" and the package name is "collection". The short-hand notation for referencing this package in the code is "col".

Package Names

The Go best practices suggest that we keep package names short so that they are easy to reference in our code. The problem is that the resulting import section can be rather cryptic:

import (
	"bufio"
	"comp"
	"expvar"
	"fmt"
	"os"
	"strconv"

Instead, we recommend longer, descriptive package names and assign to each a short three character variable name—usually the first three letters of the package name excluding any version suffix. This makes package references in the code terse—but still recognizable—while making the import section informative. Here is an example:

import (
	fmt "fmt"
	col "github.com/craterdog/go-collection-framework/v4"
	mod "github.com/craterdog/go-model-framework/v4"
	osx "os"
	str "strings"
)

Notice that for consistency, we even use this convention for package names that are only three characters long (or less) like "fmt" and "os". This import naming convention allows the actual package that is imported to be changed—as when new versions of the package are are released over time—without having to update each place in the code that references it.

Package Exports

The code within a Go module depends on other Go packages. Anything named with an uppercase identifier is automatically exported from a package. This means that it is very important to know what our package is exporting and make sure that no variables or functions are accidentally exported—since this can cause security and threading issues.

⚠️ To make the exports easier to track, we recommend that each package directory within a module contain a file named Package.go that documents the purpose of the package and defines the exported abstract definitions and interfaces. For example:

/*
................................................................................
.    Copyright (c) 2009-2024 Crater Dog Technologies.  All Rights Reserved.    .
................................................................................
.  DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.               .
.                                                                              .
.  This code is free software; you can redistribute it and/or modify it under  .
.  the terms of The MIT License (MIT), as published by the Open Source         .
.  Initiative. (See https://opensource.org/license/MIT)                        .
................................................................................
*/

/*
Package <name> provides...

This package follows the Crater Dog Technologies™ Go Coding Conventions located
here:
  - https://github.com/craterdog/go-model-framework/wiki

Additional concrete implementations of the classes defined by this package can
be developed and used seamlessly since the interface definitions only depend on
other interfaces and primitive types—and the class implementations only depend
on interfaces, not on each other.
*/
package <name>

// Types
...

// Functionals
...

// Aspects
...

// Classes
...

// Instances
...

Within each minor section (...) of the Package.go file the type and interface declarations should be alphabetized to make them easier to find. We will cover each of these sections—in great detail—in subsequent sections of this document.

Since the purpose of the Package.go file is to define what is exported from the package, it basically defines the interface to the package. This means that when something changes in the Package.go file for one or more of the packages in a module—the version number of the module should be updated as follows:

  • Major Version - When any of the existing exported definitions change or are removed, the major version should be incremented (e.g. v2.3.4 -> v3.0.0) since it is no longer backward compatible with previous versions.
  • Minor Version - When any new definitions are added to a package, the minor version should be incremented (e.g. v2.3.4 -> v2.4.0) to show that there is new functionality in the module.
  • Micro Version - When no changes were made to the Package.go file—only to the class implementations to fix bugs or make them more efficient—the micro version should be incremented (e.g. v2.3.4 -> v2.3.5).

To view or download a code template for a Package.go file, click here

Package Definitions

There are some simple package definitions that do not require any additional implementation details to be defined for them. These abstractions should be defined in the Package.go file.

Type Definitions

A primitive Go data type (int, float64, string, etc.) may be constrained for use in the method signatures subsequently defined in the package interfaces. Here are a couple quick examples:

// Types

/*
Identifier is a constrained type representing a generic identifier that can be
used to label things.  An Identifier cannot contain any whitespace.
*/
type Identifier string

/*
Ordinal is a constrained type representing an ordinal number in the range [1..∞).
The value `0` is used to represent infinity.
*/
type Ordinal uint64

The constrained types are still primitive Go data types and can be used anywhere the base data type can be used in Go code. For example:

	var identifier Identifier = "Gopher"
	var language = identifier[0:2]
	var ordinal Ordinal = 5
	var six = ordinal + 1

A constrained type may also be used as an enumerated type. The constrained type specifies the type of each value in the enumerated type. Our Angle class example demonstrates this approach:

// Types

/*
Units is a constrained type representing the possible units for an angle.
*/
type Units uint8

const (
	Degrees Units = iota
	Radians
	Gradians
)

Using the iota keyword with a constrained numeric data type allows the values to be automatically initialized starting with 0.

Note that since these constrained types are still primitive Go data types we cannot define a constructor for them that constrains the input values for these types. To do this we need to define a class based on of one of the primitive Go data types.

To view or download a code template for a type definition click here

Functional Definitions

The signature of a function may be used in a package definition. This makes it easy to pass different implementations of a function as an argument to another function. And just like all Go types are named using a noun phrase, functional definition names end with the word "Function" to turn them into a noun phrase. Here is an example of a functional definition from our Catalog[K, V] class:

// Functionals

/*
RankingFunction[V any] defines the signature for any function that can
determine the relative ordering of two values. The result must be one of the
following:

	-1: The first value is less than the second value.
	 0: The first value is equal to the second value.
	 1: The first value is more than the second value.

The meaning of "less" and "more" is determined by the specific function that
implements this signature.
*/
type RankingFunction[V any] func(
	first V,
	second V,
) int

To view or download a code template for a functional definition click here

Package Interfaces

We abstract out a package's class-based design into a set of aspect, class, and instance interface definitions. We are then free to develop concrete classes that implement these interfaces.

Usually, an interface describes one or more aspects of an abstract type. Therefore, we use an adjective phrase to name each interface defined using our class-based model. This is different from the approach often used within the Go standard library which just adds "er" to the end of a method name within an interface. In general—though not always—an interface with a single method does not make much sense.

Some of the Go language packages contain interfaces that are named using noun phrases. But since an interface generally describes only one aspect of a class, an adjective phrase seems more appropriate. These are departures from the recommended Go interface naming conventions.

⚠️ A "fun fact" about the Go language is that, in spite of the fact that Go has the concept of an interface, under–the–covers Go really deals with formal—as in mathematical—sets of method signatures. If two Go interfaces define the same set of method signatures they are interpreted as the same interface—even if the semantics behind each interface are completely different. It's true! Check it out in the Go playground here.

This is why Go does not provide an explicit way to tell the compiler that a concrete class implements a specific Go interfaces. The compiler won't tell us if we have forgotten to implement—in a concrete class—one of the Go interfaces. The compiler won't notice when a new method signature has been added to an existing Go interface either.

Fortunately, there is a nice way to structure our package interfaces such that the compiler will notice when a method is missing from a concrete class. Each class constructor method for a concrete class should return an interface rather than a reference to the concrete class structure. The interface—which contains all the required methods—is then used to call methods on a specific instances of the concrete class. Class constructors are defined in the class interface for a class.

Aspect Interfaces

We use the concept of an aspect interface to group together a set of cohesive instance methods that highlight a single aspect of a class. A well designed aspect interface conveys this cohesiveness quickly and concisely.

The aspect interface for our Angle class that was demonstrated above is as follows:

// Aspects

/*
Angular is an aspect interface that defines a set of method signatures that
must be supported by each instance of an angular concrete class.
*/
type Angular interface {
	// Methods
	AsNormalized() AngleLike
	InUnits(units Units) AngleLike
}

The interface defines only a subset of the instance methods required by the class's full instance interface. And, as required, this aspect interface depends exclusively upon abstract types.

Here is another example showing a slightly more complex aspect interface from our Catalog[K, V] example:

// Aspects

/*
Sequential[V any] is an aspect interface that defines a set of method signatures
that must be supported by each instance of a sequential concrete class.
*/
type Sequential[V any] interface {
	// Methods
	AsArray() []V
	GetSize() int
	IsEmpty() bool
}

This example shows how generics can be integrated into an interface definition. Note that before we can use this aspect interface we must bind an actual type to the generic type V.

And one more example of a generic aspect interface from our Catalog[K, V] example:

// Aspects

/*
Associative[K comparable, V any] is an aspect interface that defines a set of
method signatures that must be supported by each instance of an associative
concrete class.
*/
type Associative[K comparable, V any] interface {
	// Methods
	AddAssociation(association AssociationLike[K, V])
	GetValue(key K) V
	RemoveValue(key K) V
	SetValue(key K, value V)
}

Notice that the Associative[K, V] aspect interface depends on the generic AssociationLike[K, V] interface. Again, each interface only depends on other abstract types.

To view or download a code template for an aspect interface click here

Class Interfaces

The Go language does not provide a scoping mechanism at the type (or file) level. We fill this gap by creating an abstract class interface for each class that defines the constants, constructors and functions that must be supported by each concrete class that implements the class interface. Here is what the class interface for our Angle class looks like:

// Classes

/*
AngleClassLike is a class interface that defines the set of class constants,
constructors and functions that must be supported by each angle-like concrete
class.
*/
type AngleClassLike interface {
	// Constants
	Pi() AngleLike
	Tau() AngleLike

	// Constructors
	MakeWithValue(value float64) AngleLike
	MakeFromString(value string) AngleLike

	// Functions
	Sine(angle AngleLike) float64
	Cosine(angle AngleLike) float64
	Tangent(angle AngleLike) float64
}

⚠️ All three concepts are defined as methods on the class itself—not on instances of the class. This is similar to the concept of static methods in other, object-oriented, languages.

As mentioned earlier, since constants are "things", they are named using noun phrases. Each constructor name begins with the word "Make" to make it a verb phrase, and each function may be either a noun phrase or a verb phrase depending on what it does and what it returns. In this example each function name is a noun since each defines a mathematical concept.

Notice that the methods in the AngleClassLike interface all refer to the abstract AngleLike interface rather than on a concrete Angle class implementation. This keeps the interface abstract as required by our maxims.

The following code demonstrates the use of the Angle concrete class which implements the AngleClassLike class interface:

	// Retrieve the angle class.
	var Angle = Angle()

	// Retrieve a class constant.
	var pi = Angle.Pi()

	// Call a class constructor.
	var angle = Angle.MakeWithFloat(-1.23)

	// Call some class functions.
	var sineOfAngle = Angle.Sine(angle)
	var cosineOfPi = Angle.Cosine(pi)

	// Call some instance methods.
	var normalized = angle.AsNormalized()
	var degrees = angle.InUnits(Degrees)
	var radians = angle.InUnits(Radians)

This example takes advantage of the Units enumerated type defined in the types section of this document. The example also shows instance methods being called. For the Angle class these methods are specified in the Angular aspect interface defined earlier.

To view or download a code template for a class interface click here

Instance Interfaces

An instance interface defines the full set of methods that may be called on an instance of a concrete class. This may include the methods defined one or more aspect interfaces. For example, our concrete Angle class must support both of the aspect interfaces defined in the AngleLike instance interface:

// Instances

/*
AngleLike is an instance interface that defines the complete set of attributes,
abstractions and methods that must be supported by each instance of a concrete
angle-like class.
*/
type AngleLike interface {
	// Attributes
	GetClass() AngleClassLike
	GetValue() float64

	// Abstractions
	Angular

	// Methods
	IsZero() bool
	AsString() string
}

An instance interface may contain only the attribute access methods that make up its interface. Here is an example showing the instance interface defined for our example Association[K, V] class:

// Instances

/*
AssociationLike[K comparable, V any] is an instance interface that defines
the complete set of instance attributes, abstractions and methods that must be
supported by each instance of a concrete association-like class.

This type is parameterized as follows:
  - K is a primitive type of key.
  - V is any type of value.

This type is used by catalog-like instances to maintain their associations.
*/
type AssociationLike[K comparable, V any] interface {
	// Attributes
	GetClass() AssociationClassLike[K, V]
	GetKey() K
	GetValue() V
	SetValue(value V)
}

This instance interface defines the method signatures for all methods that must be implemented by each AssociationLike[K, V] concrete class. Notice that the key attribute is read-only but the value attribute is writeable. Again, before we can use this instance interface we must bind actual types to the generic types K and V.

⚠️ Each class in our class-based model should define a read-only attribute referencing the class itself. This allows a program that is passed an instance of a class to access the instance's actual class reference—including any constants, constructors or functions for that class.

And finally, an instance interface may be made up of multiple abstract interfaces and also include additional methods that define its public interface, for example:

// Instances

/*
CatalogLike[K comparable, V any] is an instance interface that defines the
complete set of instance attributes, abstractions and methods that must be
supported by each instance of a concrete catalog-like class.

This type is parameterized as follows:
  - K is a primitive type of key.
  - V is any type of entity.

A catalog-like class can use any association-like class key-value association.
*/
type CatalogLike[K comparable, V any] interface {
	// Attributes
	GetClass() CatalogClassLike[K, V]

	// Abstractions
	Associative[K, V]
	Sequential[AssociationLike[K, V]]

	// Methods
	SortValues(ranker RankingFunction)
}

In this example the Sequential[V] aspect interface is bound to the AssociationLike[K, V] instance interface as its value for type V. This means that this instance interface works for any sequence of association-like values.

The SortValues method signature depends on the RankingFunction functional type defined earlier in the functional definitions section of this document.

⚠️ Observe that if a new method is added to the CatalogLike[K, V] instance interface, or any of its aspect interfaces, the compiler will generate an error when compiling any CatalogLike[K, V] concrete class that fails to implement the new method. This is a pretty useful technique!

To view or download a code template for an instance interface click here

This completes our discussion of the three kinds of the abstract interfaces:

Thus far, we have only discussed package abstractions—what classes looks like from the outside. Now it is time to look at implementing these classes. We can, of course, implement each class manually using the class templates provided here. But we also provide a code generator that will generate all of the classes defined in a Package.go file. The generated classes will automatically comply with the coding standards defined in this document. It really is a better—and much faster—way to code. For details on the code generator, click here

Classes

The class model defined by the package interfaces are implemented by concrete classes. Since each class represents a well defined "thing", classes are named using noun phrases. Each concrete class implementation should be in its own file with the same name, but all lowercase. The following shows the format for a typical concrete class file:

/*
................................................................................
.    Copyright (c) 2009-2024 Crater Dog Technologies.  All Rights Reserved.    .
................................................................................
.  DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.               .
.                                                                              .
.  This code is free software; you can redistribute it and/or modify it under  .
.  the terms of The MIT License (MIT), as published by the Open Source         .
.  Initiative. (See https://opensource.org/license/MIT)                        .
................................................................................
*/

package <name>

import (
	fmt "fmt"
	...
)

// CLASS ACCESS

// Reference
...

// Function
...

// CLASS METHODS

// Target
...

// Constants
...

// Constructors
...

// Functions
...

// INSTANCE METHODS

// Target
...

// Attributes
...

// <FirstAspectName>
...

// <SecondAspectName>
...

// <LastAspectName>
...

// Public
...

// Private
...

To view or download a code template for a class file, click here

Class Access

A public class function is used by other Go modules that depend on a class to gain access to the class. The class itself is defined as a reference to a private structure containing the class constants. The reference to the structure is returned—as an interface—by the class function.

Here is an example of the public class function and private structure for our Angle class:

// CLASS ACCESS

// Reference

var angleClass = &angleClass_{
	pi_:  angle_(mat.Pi),
	tau_: angle_(2.0 * mat.Pi),
}

// Function

func Angle() AngleClassLike {
	return angleClass
}

⚠️ Since the class is always referenced as an AngleClassLike class interface, the private structure—which implements the class interface—is protected while allowing access to the public constants, constructors and functions for the class. And, since the reference to the private structure is read-only, there are no issues with multi-threaded applications.

In the example, the private class constant values are initialized in the class declaration—at package load time—using named arguments. This is recommended so that the order of the arguments does not matter, and the initialization is not affected when new constants are added to the concrete class.

The class function and private structure definitions for our Association[K, V] and Catalog[K, V] classes are a bit more complicated due to the fact that these classes support generics. The private structures for the specific (i.e. bound to actual types) generic classes cannot be initialized ahead of time. The class function and private structure definitions themselves need to be generic, as well, to ensure that a reference to a specific class interface is returned by the generic class function.

The public class function and private structure for our Association[K, V] class demonstrate this:

// CLASS ACCESS

// Reference

var associationClass = map[string]any{}
var associationMutex syn.Mutex

// Function

func Association[
	K comparable,
	V any,
]() AssociationClassLike[K, V] {
	// Generate the name of the bound class type.
	var class AssociationClassLike[K, V]
	var name = fmt.Sprintf("%T", class)

	// Check for existing bound class type.
	associationMutex.Lock()
	var value = associationClass[name]
	switch actual := value.(type) {
	case *associationClass_[K, V]:
		// This bound class type already exists.
		class = actual
	default:
		// Add a new bound class type.
		class = &associationClass_[K, V]{
			// This class has no private constants to initialize.
		}
		associationClass[name] = class
	}
	associationMutex.Unlock()

	// Return a reference to the bound class type.
	return class
}

⚠️ Notice that this code is considerably more complex than the Angle example. The creation and initialization of the reference to the private structure must happen in the public class function rather than in the reference declaration. This is due to the fact that the parameter types for K and V are not known until the class function is called.

A private Go map variable is used to map the specific bound class names to the single references for each bound class. Since this map is a global variable that gets updated with new mappings over time, it must be protected by a mutex to avoid multi-threading issues. If not, the following error will occur periodically:

fatal error: concurrent map read and map write

The class function completely hides this complexity from any module that is using it. For example, to use the Association[K, V] class function to return a reference to a specific type of association class—say for string keys and int values—and use it to create and modify an association, we do the following:

	// Retrieve a specific association class.
	var Association = Association[string, int]()

	// Create a new association.
	var key string = "answer"
	var value int = 42
	var association = Association.MakeWithAttributes(key, value)

	// Change the value of the association.
	association.SetValue(25)

To view or download a code template for a generics-based class file, click here

Class Methods

The methods defined in a class interface are implemented as class methods by a concrete class. The class function returns a reference to this concrete class as a class interface which defines all of the class methods that the concrete class implements.

Class Target

Every method requires a target on which to operate. The target for a class method is the class itself rather than specific instances of a class as with instance methods.

Each concrete class defines a class target that is a private Go structure type whose attributes define any class constant values. Here is an example of the private structure type definition for our Angle concrete class:

// CLASS METHODS

// Target

type angleClass_ struct {
	pi_  AngleLike
	tau_ AngleLike
}

As usual, the private attributes representing the class constants reference abstract type definitions.

Each class method declaration for a concrete class will have the same form:

func (c *<classname>Class_) <MethodName>(<parameters>) <AbstractType> {
	...
}

⚠️ To make it easy to spot the class target in each class method implementation we use the single character variable "c" (for "class") for the name of the class target. Also, since the class target is a Go structure it is passed by reference into each class method.

We will now look at each of the different kinds of class methods that operate on the class target.

Constant Methods

Since the class constants defined in the class structure are private, we need a way for dependent code modules to access these constant values without being able to modify them. We define a public class method on the private class structure for each class constant. The Angle class defines two of these as follows:

// Constants

func (c *angleClass_) Pi() AngleLike {
	return c.pi_
}

func (c *angleClass_) Tau() AngleLike {
	return c.tau_
}

If you are curious about the Tau constant, click here.

Any Go code module that is dependent upon the Angle class will access its constants like this:

	// Retrieve the angle class reference.
	var Angle = Angle()

	// Access its constant values.
	var pi  = Angle.Pi()
	var tau = Angle.Tau()

To view or download a code template for a class constant, click here

Constructor Methods

A class is used to define one or more constructor methods for creating new instances of the class. This is analogous to the constructors in most object-oriented languages. In the Angle class we define the following constructor methods:

// Constructors

func (c *angleClass_) MakeFromFloat(float float64) AngleLike {
	return angle_(float)
}

func (c *angleClass_) MakeFromString(string_ string) AngleLike {
	var angle AngleLike
	switch string_ {
	case "pi", "π":
		angle = c.pi_
	case "tau", "τ":
		angle = c.tau_
	default:
		var message = fmt.Sprintf(
			"Attempted to construct an angle from an invalid string: %v",
			string_,
		)
		panic(message)
	}
	return angle
}

The second example returns the class constants for their corresponding string values.

⚠️ Notice that since string is a Go keyword, we append an underscore to the parameter name. We use this approach consistently for any identifiers that conflict with Go keywords.

A code module that depends on the Angle class can construct new instances like this:

	// Retrieve the angle class reference.
	var Angle = Angle()

	// Construct new instances of angles.
	var angle = Angle.MakeFromFloat(5.12)
	var delta = Angle.MakeFromString("π")

To view or download a code template for a class constructor, click here

Since our example Association[K, V] class is based on a Go structure instead of a Go primitive type, the constructor method is a little more complex:

func (c *associationClass_[K, V]) MakeWithAttributes(
	key K,
	value V,
) AssociationLike[K, V] {
	return &association_[K, V]{
		class_: c,
		key_:   key,
		value_: value,
	}
}

⚠️ An instance of a structure is created and its attributes are initialized using the class target c and the two arguments passed into the constructor method. Again, the private attribute values are initialized using named arguments so that the order does not matter and any attributes that should default to their "zero" values can be skipped.

To view or download a code template for a generics-based class constructor, click here

Function Methods

Often we need a library of functions that operate on one or more instances of a class. These functions should, therefore, be scoped to the class rather than to the package. These class functions are implemented as methods on the class. Here is an example of the class functions for our example Angle class:

// Functions

func (c *angleClass_) Sine(angle AngleLike) float64 {
	return mat.Sin(angle.AsFloat())
}

func (c *angleClass_) Cosine(angle AngleLike) float64 {
	return mat.Cos(angle.AsFloat())
}

func (c *angleClass_) Tangent(angle AngleLike) float64 {
	return mat.Tan(angle.AsFloat())
}

As before, any code module that is dependent upon the Angle class will call these class functions like this:

	// Retrieve the angle class reference.
	var Angle = Angle()

	// Retrieve the angle pi.
	var angle = Angle.Pi()

	// Call the class functions using the angle.
	var sine = Angle.Sine(angle)
	var cosine = Angle.Cosine(angle)
	var tangent = Angle.Tangent(angle)

⚠️ It is important to remember that class methods operate on the private class structure rather than on specific instances of the class.

To view or download a code template for a class function, click here

Instance Methods

Now it is time to look at the instances of a concrete class—as opposed to the concrete class itself. At run-time many instances of a concrete class may be created and used. Each instance maintains its own private set of attribute values—but all instances share the same set of instance method implementations corresponding to the methods defined in the instance interface for the concrete class.

A concrete class encapsulates one or more private attributes in a way that protects the access to the attributes according to the constraints put on the concrete class by the instance interface that it implements. The instance methods implemented by the concrete class enforce these constraints.

Since instance methods act on instances of concrete classes they are named using verb phrases. This naming convention differs slightly from the Go recommended conventions but—as mentioned before—we think that clarity and consistency are more important than brevity.

Instance Target

Unlike a class target, the target for an instance method is a specific instance value of a concrete class.

There are two possible kinds of instance value targets:

  1. A private variable that is a primitive Go data type (int, string, []byte, etc.).
  2. A reference to a private Go struct containing the private attributes for an instance value.

Here is an example of the first kind of instance value target. It is a private primitive type definition that is used for each instance of our Angle concrete class:

// INSTANCE METHODS

// Target

type angle_ float64

This type acts like a single instance attribute that can be extended into a concrete class by adding instance methods that implement an instance interface while preserving its primitive Go data type compatibility with the built-in operations for the Go language.

Each instance method implementation for a primitive instance value target will have the form:

func (v <classname>_) <MethodName>(<parameters>) <AbstractType> {
	...
}

⚠️ To make it easy to spot the instance value target in each instance method implementation we use the single character variable "v" (for "value") for the name of the instance value target.

Since—in this case—the instance value target is a primitive Go data type it is passed by value (no "*") into an instance method.

To view or download a code template for the extended primitive target and its instance methods, click here

Now an example of the second kind of instance target. A private Go struct type definition is used to create each instance of our Association[K, V] concrete class:

// INSTANCE METHODS

// Target

type association_[
	K comparable,
	V any,
] struct {
	class_ AssociationClassLike[K, V]
	key_   K
	value_ V
}

This kind of instance target value may encapsulate multiple private attributes. The first attribute in this example is the class reference attribute returned by The GetClass() method. Each private attribute is of an abstract type, or—as in this example—a generic type.

⚠️ For encapsulated structures the target instance value is a reference to the structure. Therefore, the instance method declaration for this kind of instance value target will include the "*":

func (v *<classname>_) <MethodName>(<parameters>) <AbstractType> {
	...
}

Again, the target value in an instance method for this encapsulated structure is named "v".

To view or download a code template for an encapsulated structure target and its instance methods, click here

We will now look at the different kinds of instance methods that operate on instance target values.

Attribute Methods

Methods are used to protect the private attributes defined by a concrete class. They are grouped together in their own section—in the concrete class—following the instance target declaration.

The following example shows the attribute method implementations for the Association[K, V] class:

// Attributes

func (v *association_[K, V]) GetClass() AssociationClassLike[K, V] {
	return v.class_
}

func (v *association_[K, V]) GetKey() K {
	return v.key_
}

func (v *association_[K, V]) GetValue() V {
	return v.value_
}

func (v *association_[K, V]) SetValue(value V) {
	v.value_ = value
}

⚠️ This example shows the implementation of the standard GetClass() attribute method that is defined by our class-based model.

This example also demonstrates two getter methods and one setter method. Getter and setter methods allow a code module that uses a concrete class to retrieve and set (respectively) the attributes on an instance value. The name of a getter instance method always begins with either "Is" (for boolean attributes) or "Get" followed by the attribute name. The name of a setter instance method always begins with "Set" followed by the attribute name.

⚠️ Note that just because a concrete class has a getter method for an attribute doesn't mean that it should also have a setter method for the same attribute. Some attributes are designed to be read–only. A much less common scenario is for an attribute—like a password—to be update–only. In this case, there is only a setter method and no getter method for the attribute.

Aspect Methods

A concrete class that implements a specific aspect interface must provide an implementation for each instance method defined in that aspect interface. We group these aspect method implementations together with one section for each aspect following the attribute methods section. The section name for each aspect is the aspect name and the aspect sections should be alphabetized to make it easier to find a specific section.

The following example is of an aspect method implementation from the Catalog[K, V] concrete class:

// Associative

func (v *catalog_[K, V]) RemoveValues(keys Sequential[K]) Sequential[V] {
	var values = List[V]().Make()
	var iterator = keys.GetIterator()
	for iterator.HasNext() {
		var key = iterator.GetNext()
		values.AppendValue(v.RemoveValue(key))
	}
	return values
}

This is an example of an action method. Action methods perform some action on the target instance value. All action methods should be named with a verb phrase denoting the action being performed. Here are some other examples of action methods:

  • list.ShuffleValues()
  • stack.RemoveTop() V
  • scanner.EmitToken(token *Token)

⚠️ If the action that is performed involves more than one instance of the concrete class, it should be implemented as a class function rather than an instance method.

Public Methods

Any public instance method implemented in a concrete class that doesn't manage the access to instance attributes or belong to an aspect interface is called a public method. Public methods are grouped together in a section following the last aspect method section.

The following example shows the public method implementations for the Angle class:

// Public

func (v angle_) IsZero() bool {
	return v == 0
}

func (v angle_) AsString() string {
	return float64(v)
}

The first public instance method is an example of a question method. A question method determines whether or not something is true about the state of the instance value. Question methods generally—though not always—begin with a "to-be" verb (i.e. "Is", "Am", "Are", "Was", "Were", "Been", "Being") followed by the condition that is being checked. Here are some other examples of question methods:

  • IsEmpty() bool
  • AreReady() bool
  • WasCancelled() bool
  • BeingProcessed() bool
  • HasFailed() bool
  • MatchesText(text string) bool

The second public instance method is an example of a transformer method. A transformer method transforms the state of an instance value into a different type of value. The name of a transformer method always begins with "As" followed by the desired type name.

Private Methods

The final kind of instance method to be discussed are the private instance methods. Private instance methods are only called by other instance methods and focus on specific tasks. Each private method name must begin with a lowercase letter so that it is not exported by the package. Other than its case, a private method follows the same method naming conventions mentioned earlier in this document.

Our last code example shows a private instance method from example Angle concrete class that captures the algorithm for normalizing the value of an angle.

// Private

func (v angle_) normalize(angle float64) float64 {
	var tau = 2.0 * mat.Pi
	if angle <= -tau || angle >= tau {
		// Normalize the angle to the range (-τ..τ).
		angle = mat.Remainder(angle, tau)
	}
	if angle == -0 {
		// This is annoying so fix it.
		angle = 0
	}
	if angle < 0.0 {
		// Normalize the angle to the range [0..τ).
		angle = angle + tau
	}
	return angle
}

This private instance method is used—among other places—in the implementation of the Angle concrete class's angle.AsNormalized() aspect instance method.

Modules Revisited

We have now journeyed through the following key concepts related to our Go class-based model:

  • modules
  • packages
  • classes
  • constants
  • constructors
  • functions
  • methods

For each concept we have established coding conventions and viewed example code that illustrates these conventions. Now it is time to revisit the first topic, modules...

Since a Go module may contain multiple packages, each with its own set of classes, and each class with its own set of constants, constructors, functions and methods; the extent of the things being exported by a Go module—though very powerful—may get pretty complicated. But we want our Go modules to be easy to use—not just powerful.

⚠️ Fortunately, there is a way that we can get the best of both worlds by creating a Module.go file—at the root level of each version of a module—that exports the module-level definitions for that version of the module. This allows any Go program that wants to use the packages in the module to only include the module itself rather than all the packages provided by the module.

Type Aliases

The Go language allows for—though discourages—the concept of a type alias. A type alias defines another name for a type that can be used anywhere the type can be used, for example:

type Angle = tri.Angle

This is only recommended by the Go development team when migrating code in a graceful manner and not as a general practice. However, in our case defining type aliases in our Module.go file provides a clean way to export package-level types as module-level types. In other words, we "promote" the exported types from all of the packages in our module as module level-types.

⚠️ It is important to note that the Go language does not yet support type aliases for generic types. This is a shame since it means we cannot do the following:

type CatalogLike = col.CatalogLike

Until the support for generic aliases is added go Go, all generic types will need to be referenced by imports at the package-level rather than at the module-level 😔.

Unified Constructors

Using a similar approach we can consolidate all of the constructor methods for a class into a single unified constructor—for that class—that is exported at the module-level in our Module.go file. Here is an example of that pattern for our Catalog[K, V] class which defines multiple constructors at the package level:

func Catalog[K comparable, V any](arguments ...any) col.CatalogLike[K, V] {
	// Initialize the possible arguments.
	var associations []col.AssociationLike[K, V]
	var mappings map[K]V
	var sequence col.Sequential[col.AssociationLike[K, V]]
	var source string

	// Process the actual arguments.
	for _, argument := range arguments {
		switch actual := argument.(type) {
		case []col.AssociationLike[K, V]:
			associations = actual
		case map[K]V:
			mappings = actual
		case col.Sequential[col.AssociationLike[K, V]]:
			sequence = actual
		case string:
			source = actual
		default:
			var message = fmt.Sprintf(
				"Unknown argument type passed into the catalog constructor: %T\n",
				actual,
			)
			panic(message)
		}
	}

	// Call the right constructor.
	var class = col.Catalog[K, V]()
	var catalog col.CatalogLike[K, V]
	switch {
	case len(associations) > 0:
		catalog = class.MakeFromArray(associations)
	case len(mappings) > 0:
		catalog = class.MakeFromMap(mappings)
	case sequence != nil:
		catalog = class.MakeFromSequence(sequence)
	case len(source) > 0:
		catalog = class.MakeFromSource(source)
	default:
		catalog = class.Make()
	}
	return catalog
}

This unified constructor allows us to construct an instance of the Catalog[K, V] class in multiple ways:

import (
	col "github.com/craterdog/go-collection-framework/v4"
)

func main() {
	// Create a new catalog from a Go map.
	var catalog = col.Catalog[string, int32](map[string]int{
		"alpha": 'α',
		"beta":  'β',
		"gamma": 'γ',
		"delta": 'δ',
	})

	// Create another catalog from a source string.
	catalog = col.Catalog[string, int32](
`[
    "foo": 1
    "bar": 2
    "baz": 3
](Catalog)`
	)
}

The combination of module-level type aliases with unified constructors makes the general use of a class-based module clean and simple.

Code Generation

⚠️ Since our class-based model, and our coding conventions, were designed to be simple—it wasn't too hard to take the next step and create a code generator that enforces the coding conventions while generating each concrete class associated with its abstract interfaces defined in the Package.go file. The process is very straight forward.

Installing the Code Generator

The code generator is one of several tools that can be run as stand-alone programs by installing the go-model-tools module as follows:

$ git clone git@github.com:craterdog/go-model-tools.git
Cloning into 'go-model-tools'...
...
Resolving deltas: 100% (56/56), done.

$ tools=`pwd`/go-model-tools/

$ cd ${tools}

$ go etc/build.sh

$ cd

Creating a New Class Model

A new Package.go file can be created that contains an example of each type of aspect, class and instance interface definition. To create one do the following:

$ mkdir example

$ ${tools}/bin/initialize
Usage: initialize <directory> <name> <copyright>

$ ${tools}/bin/initialize example/ example ""

$ cat example/Package.go
/*
................................................................................
.                   Copyright (c) 2024.  All Rights Reserved.                  .
................................................................................
.  DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.               .
.                                                                              .
.  This code is free software; you can redistribute it and/or modify it under  .
.  the terms of The MIT License (MIT), as published by the Open Source         .
.  Initiative. (See https://opensource.org/license/MIT)                        .
................................................................................
*/

/*
Package "example" provides...

This package follows the Crater Dog Technologies™ Go Coding Conventions located
here:
  - https://github.com/craterdog/go-model-framework/wiki

Additional implementations of the concrete classes provided by this package can
be developed and used seamlessly since the interface definitions only depend on
other interfaces and primitive types—and the class implementations only depend
on interfaces, not on each other.
*/
package example

// Types

/*
TokenType is a constrained type representing any token type recognized by a
scanner.
*/
type TokenType uint8

const (
	ErrorToken TokenType = iota
	CommentToken
	DelimiterToken
	EOFToken
	EOLToken
	IdentifierToken
	NoteToken
	SpaceToken
	TextToken
)

// Functionals

/*
RankingFunction[V any] is a functional type that defines the signature for any
function that can determine the relative ordering of two values. The result must
be one of the following:

	-1: The first value is less than the second value.
	 0: The first value is equal to the second value.
	 1: The first value is more than the second value.

The meaning of "less" and "more" is determined by the specific function that
implements this signature.
*/
type RankingFunction[V any] func(
	first V,
	second V,
) int

// Aspects

/*
Sequential[V any] is an aspect interface that defines a set of method signatures
that must be supported by each instance of a sequential concrete class.
*/
type Sequential[V any] interface {
	// Methods
	AsArray() []V
	GetSize() int
	IsEmpty() bool
}

// Classes

/*
SetClassLike[V any] is a class interface that defines the complete set of
class constants, constructors and functions that must be supported by each
concrete set-like class.

The following functions are supported:

And() returns a new set containing the values that are both of the specified
sets.

Or() returns a new set containing the values that are in either of the specified
sets.

Sans() returns a new set containing the values that are in the first specified
set but not in the second specified set.

Xor() returns a new set containing the values that are in the first specified
set or the second specified set but not both.
*/
type SetClassLike[V any] interface {
	// Constants
	Ranker() RankingFunction

	// Constructors
	Make() SetLike[V]
	MakeFromArray(values []V) SetLike[V]
	MakeFromSequence(values Sequential[V]) SetLike[V]
	MakeFromSource(source string) SetLike[V]

	// Functions
	And(
		first SetLike[V],
		second SetLike[V],
	) SetLike[V]
	Or(
		first SetLike[V],
		second SetLike[V],
	) SetLike[V]
	Sans(
		first SetLike[V],
		second SetLike[V],
	) SetLike[V]
	Xor(
		first SetLike[V],
		second SetLike[V],
	) SetLike[V]
}

// Instances

/*
SetLike[V any] is an instance interface that defines the complete set of
instance attributes, abstractions and methods that must be supported by each
instance of a concrete set-like class.  A set-like class maintains an ordered
sequence of values which can grow or shrink as needed.

This type is parameterized as follows:
  - V is any type of value.

The order of the values is determined by a configurable ranking function.
*/
type SetLike[V any] interface {
	// Attributes
	GetClass() SetClassLike[V]

	// Abstractions
	Sequential[V]

	// Methods
	AddValue(value V)
	AddValues(values Sequential[V])
	RemoveAll()
	RemoveValue(value V)
	RemoveValues(values Sequential[V])
}

This new Package.go file can be modified to include the actual aspect, class and instance interface definitions required by the package.

Validating a Class Model

A class model definition may be syntactically correct but violate some semantic requirements of our class-based model. Our updated Package.go file can be validated as follows:

$ ${tools}/bin/validate
Usage: validate <model-file>

$ ${tools}/bin/validate example/Package.go

If any definition is not valid it will be flagged.

Reformatting a Class Model

There is a canonical format for our class-based model. Our updated Package.go file can be reformatted using that canonical format by doing the following:

$ ${tools}/bin/format
Usage: format <model-file>

$ ${tools}/bin/format example/Package.go

Generating Concrete Classes

Concrete Go classes can be automatically generated from the Package.go file for the package as follows:

$ ${tools}/bin/generate
Usage: generate <directory>

$ ${tools}/bin/generate example/

$ tree example/
example/
├── Package.go
└── ...

The generated concrete classes contain skeletons for all the public methods required of the class. The implementation of most of these methods must be added along with any private methods that are required.

Summary

We have reached the end of our Go class-based coding journey. To the simple and efficient lower-level Go language concepts we have added our own higher-level, class-based model with specific idioms and naming conventions. To make things easier still—we added code generation to jump-start our Go projects.

The resulting concepts from the class-based model are neatly grouped into three exported namespaces:

We demonstrated each of these concepts using three simplified class examples that can be viewed and experimented with in the Go playground:

Following this class-base model and our coding conventions makes writing—and generating—good Go code modules easier to do, understand and maintain. To access the code templates for any of these class-based components, click on the links listed in the side bar in the upper right corner ↗️ of this page.

We end the way we began—with our five maxims:

"Say what you mean, and mean what you say."

"Avoid global variables—anyone can mess with them at any time."

"Aim for a Goldie Locks level of abstraction."

"Concrete classes should only depend on abstract definitions, not the other way around."

"Don't spend time writing code that can be automatically generated."

Now, Go forth and write (or better yet, generate) beautiful code...

Please direct any questions or comments to craterdog@gmail.com