With the release of Go 1.22, the Go standard library introduced several new features. As you might have noticed in articles related to the previous release, here we mostly concentrate on the new exciting packages and features that they give us. This article will start this journey, by providing a deeper look into the implementation of the version package in Go.
Lang # The first function we are ready to examine is the Lang function. This function provides a cleaned, valid Go version as a string. In case it can’t determine the actual version, due to an invalid state of the string value, it will return an string as a result.
Lang function
func Lang(x string) string As we can see the function signature above, function expects one argument, a string, that represents a Go version. An output should be also one value, a string, as a cleaned Go version.
Lang function examples
package main import ( "fmt" "go/version" ) func main() { fmt.Println(version.Lang("go1.0")) // go1 fmt.Println(version.Lang("go1")) // go1 fmt.Println(version.Lang("go1.22.4")) // go1.22 fmt.Println(version.Lang("go1.22.3")) // go1.22 fmt.Println(version.Lang("go1.22.2")) // go1.22 fmt.Println(version.Lang("go1.22.rc1")) // fmt.Println(version.Lang("go1.22rc1")) // go1.22 fmt.Println(version.Lang("1.22")) // fmt.Println(version.Lang("wrong")) // fmt.Println(version.Lang("")) // } In the example above, we can see how the Lang function adapt the Go version string. It removes all minor versions and appearance of “release candide” phrase, and present them in the end as an official Go versions that we experienced in the past (and we might experience in the future). In cases where we provided an invalid, or empty string, the ending result will be also an empty string, as the Lang function can’t find the actual version name.
One interesting point, not just for the Long function, but, as you will see, for all functions in this package, to consider some string as a valid Go version, it needs to have a prefix go.
IsValid # The next function we are examining is the IsValid function. This function checks a string with a potential Go version and returns a boolean result that tells us if the version is valid or not.
IsValid function
func IsValid(x string) bool As we can see the function signature above, function expects one argument, a string, that represents a Go version. An output should be a bool value, which tells us if the Go version is valid or not.
With the release of Go 1.21, the Go standard library introduced several new features. While we’ve already discussed some of them in previous articles, in this episode, we’ll dive into more advanced enhancements. Naturally, we’ll focus on the new functions designed for sorting slices, which are part of the new slices package. This article will provide a deeper look into the implementation of these three new functions and touch on benchmarking as well.
Sort # The Sort function is the first one we’d like to explore. This implementation is built upon the enhanced Pattern-defeating Quicksort, positioning it as one of the best-known unstable sorting algorithms. Don’t worry; we will discuss this “instability” aspect in this article. But first, let’s take a look at the function’s signature:
Sort function
func Sort[S ~[]E, E cmp.Ordered](x S) As we’ve seen in some other articles, nearly all improvements in the Go standard library are built upon generics, a feature introduced in Go version 1.18, almost three years ago. Similar to other functions, the Sort function also expects a slice of a generic type as an argument, where each item must adhere to the Ordered constraint. The function doesn’t return a new value but sorts the original slice in place. Below, you’ll find some basic examples:
Sort function examples
ints := []int{1, 2, 3, 5, 5, 7, 9} slices.Sort(ints) fmt.Println(ints) // Output: // 1 2 3 5 5 7 9 ints2 := []int{9, 7, 5, 5, 3, 2, 1} slices.Sort(ints2) fmt.Println(ints2) // Output: // 1 2 3 5 5 7 9 floats := []float64{9, 3, 5, 7, 1, 2, 5} slices.Sort(floats) fmt.Println(floats) // Output: // 1 2 3 5 5 7 9 strings := []string{"3", "9", "2", "5", "1", "7", "5"} slices.Sort(strings) fmt.Println(strings) // Output: // 1 2 3 5 5 7 9 In the example above, we can observe the result of the Sort method. All the outputs consist of sorted slices, arranged in ascending order. However, what makes this function particularly intriguing is its ability to handle various data types using a single function, distinguishing it from the implementations we already possess in the sort package. Now that we’ve examined the results, let’s proceed to compare the performance benchmarks with the existing package.
Benchmark # In this section, we aim to evaluate the performance of the new function by comparing it to the already existing sort package. Below, you’ll find the benchmark test results:
As part of the new Go release, several exciting changes have been introduced to the Go ecosystem. While we’ve explored some of these changes in other articles about the maps package and the cmp package, there’s much more to discover beyond these two packages.
In this article, we’ll focus on the first part of the slices package, specifically its new search functionality. Like many other updates and newly introduced packages, this one is also built upon the foundation of generics, which were introduced in Go 1.18.
BinarySearch and BinarySearchFunc # Let’s start by exploring the first pair of functions designed for efficiently searching a target value within sorted slices. In this context, we’re referring to the well-known Binary Search algorithm, which is renowned as one of the most significant algorithms and is frequently used in coding interviews. Below, you’ll find the signatures of both of these functions:
BinarySearch function
func BinarySearch[S ~[]E, E cmp.Ordered](x S, target E) (int, bool) BinarySearchFunc function
func BinarySearchFunc[S ~[]E, E, T any](x S, target T, cmp func(E, T) int) (int, bool) Looking at the signatures of both functions, we can identify some small differences between them, and these differences serve specific purposes. The first function, BinarySearch, expects two arguments. The first argument should be a slice of sorted items, and it must adhere to the Ordered constraint. When the items are ordered, the algorithm can efficiently compare them using the Compare function from the cmp package.
On the other hand, the second function, BinarySearchFunc, is more versatile. It allows searching within slices where the items don’t necessarily conform to the Ordered constraint. This flexibility is achieved by introducing a third argument, the comparison function. This function is responsible for comparing items and determining their order. It will be called by the BinarySearchFunc itself to make comparisons.
Both functions return two values. The first value is the index of the item within the slice, and the second is a boolean value indicating whether the item was found in the slice or not. Let’s explore some examples below:
BinarySearch examples
fmt.Println(slices.BinarySearch([]int{1, 3, 5, 6, 7}, 5)) // Output: // 2 true fmt.Println(slices.BinarySearch([]int{1, 3, 5, 6, 7}, 9)) // Output: // 5 false fmt.Println(slices.BinarySearch([]int{1, 3, 5, 6, 7}, -5)) // Output: // 0 false fmt.Println(slices.BinarySearch([]string{"1", "3", "5", "6", "7"}, "5")) // Output: // 2 true fmt.Println(slices.BinarySearch([]string{"1", "3", "5", "6", "7", "8"}, "9")) // Output: // 6 false fmt.Println(slices.BinarySearch([]string{"1", "3", "5", "6", "7"}, "4")) // Output: // 2 false Take a close look at the results returned by the BinarySearch function, especially when the item doesn’t exist in the slice. In our examples, we encountered four such cases where the function returned 0, 2, 5, and 6. When the requested item isn’t present in the slice, the function indicates where it should be positioned if it were to be added to the slice. Since the slice is sorted, it’s possible to determine the appropriate position for the item within the slice.
As the new release of Go came this summer, many of us started to look for the improvements inside its ecosystem. Many new features were introduced, including updates to the tool command to support backward and forward compatibility. New packages appeared inside the Standard Library, including maps and slices. In this article we are covering improvements introduced with the new cmp package.
The new package offers three new functions. All of them rely on Generics, a feature introduced in Go version 1.18, which has opened up possibilities for many new features. The cmp package introduces new functions for comparing values of Ordered constraint.
Let’s dive into each of them.
Ordered constraint and Compare function # The constraint Ordered encompasses all types that support comparison operators for values, specifically, <, <=, >= and >. This includes all numeric types in Go, as well as strings.
Ordered Constraint
type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } Once we understand what the Ordered constraint includes, we can focus on the first function from the cmp package, which is the Compare function. Below, you can find its signature:
Compare Function
// Compare returns // // -1 if x is less than y, // 0 if x equals y, // +1 if x is greater than y. // ... func Compare[T Ordered](x, y T) int The signature, along with the function description, makes it much easier to understand. The Compare function expects two arguments of the same type, compares their values, and returns a result that represents the comparison status:
-1 if the first argument is less than the second. 0 if the arguments’ values are equal. 1 if the first argument is greater than the second. Let’s prove such claim:
Compare numerals
fmt.Println(cmp.Compare(1, 2)) // Output: // -1 fmt.Println(cmp.Compare(1, 1)) // Output: // 0 fmt.Println(cmp.Compare(2, 1)) // Output: // 1 Compare strings
fmt.Println(cmp.Compare("abc", "def")) // Output: // -1 fmt.Println(cmp.Compare("qwe", "qwe")) // Output: // 0 fmt.Println(cmp.Compare("abcde", "abcc")) // Output: // 1 Above, we can see practical examples of the Compare function for both numerals and strings. Indeed, the return values can only belong to the set of numbers {-1, 0, 1}, as defined in the description.
Function Less # In addition to the function Compare, we got another, similar function Less. Although it’s rather easy to understand what is used for, let’s check its signature:
Not too long ago, we witnessed a new release of our favorite programming language. The Go team didn’t disappoint us once again. They introduced numerous new features, including updates to the tool command to support backward and forward compatibility. As always, the standard library has received new updates, and the first one we’ll explore in this article is the new maps package.
The new package offers only five new functions (two additional ones were removed from the package: Values and Keys), but they provide significant value. All of them rely on Generics, a feature introduced in Go version 1.18, which has opened up possibilities for many new features. The map package clearly provides new tools for Go maps. In this particular case, it introduces new functions for checking map equality, deleting items from maps, copying items into maps, and cloning maps.
Let’s dive into each of them.
Equal and EqualFunc # First, let’s examine the pair of functions used to check map equality: Equal and EqualFunc. The first one is a straightforward function that checks the equality of two provided maps as function arguments. The second one allows you to pass an additional argument that defines how you plan to examine the equality of values inside the maps. Here are their signatures:
Function Equal
func Equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2) bool Function EqualFunc
func EqualFunc[M1 ~map[K]V1, M2 ~map[K]V2, K comparable, V1, V2 any](m1 M1, m2 M2, eq func(V1, V2) bool) bool The Equal function is easier to understand. It simply defines two generic types, M1 and M2, which represent maps of two other generic types, K and V. Obviously, K is for map keys, and it allows any comparable value. The second type is V, representing map values, and it also allows being of a comparable type.
The EqualFunc function is slightly more complicated. First, it doesn’t assume that the values in the maps are of the same type, nor do they have to be comparable. For that reason, it introduces an additional argument, which is an equality function for the values in the maps. This way, we can compare two maps that have the same keys but not the same values, and we can define the logic for comparing if they are equal.
Simple usage of Equal function
first := map[string]string{ "key1": "value1", } second := map[string]string{ "key1": "value1", } fmt.Println(maps.Equal(first, second)) // Output: // true third := map[string]string{ "key1": "value1", } fourth := map[string]string{ "key1": "wrong", } fmt.Println(maps.Equal(third, fourth)) // Output: // false In the example above, there are no surprises. We use four maps to test the Equal function. In the first case, two maps are equal, but in the second case, their values are not the same. The following example is also easy.
My favorite part of software development is writing tests, whether they are unit tests or integration tests. I enjoy the process immensely. There’s a certain satisfaction in creating a test case that uncovers a function’s failure. It brings me joy to discover a bug during development, knowing that I’ve fixed it before anyone encounters it in a test environment or, worse, in production. Sometimes, I stay up late just to write more tests; it’s like a hobby. I even spent around 30 minutes on my wedding day writing unit tests for my personal project, but don’t tell my wife!
The only thing that used to bother me was dealing with integration issues between multiple Microservices. How could I ensure that two Microservices, each with specific versions, wouldn’t face integration problems? How could I be certain that a new version of a Microservice didn’t break its API interface, rendering it unusable for others? This information was crucial to have before launching extensive scenarios in our end-to-end testing pipeline. Otherwise, we’d end up waiting for an hour just to receive feedback that we’d broken the JSON schema.
Then, one day in the office, I heard a rumor that we were planning to use Contract Testing. I quickly checked the first article I found, and I was amazed. It was a breakthrough.
Contract Testing # There are many excellent articles about Contract testing, but the one I like the most is from Pactflow. Contract testing ensures that two parties can communicate effectively by testing them in isolation to verify if both sides support the messages they exchange. One party, known as the Consumer, captures the communication with the other party, referred to as the Provider, and creates the Contract. This Contract serves as a specification for the expected requests from the Consumer and the responses from the Provider. Application code automatically generates Contracts, typically during the unit testing phase. Automatic creation ensures that each Contract accurately reflects the latest state of affairs.
Contract testing After the Consumer publishes the Contract, the Provider can use it. In its code, likely within unit tests, the Provider conducts Contract verification and publishes the results. In both phases of Contract testing, we work solely on one side, without any actual interaction with the other party. Essentially, we are ensuring that both parties can communicate with each other within their separate pipelines. As a result, the entire process is asynchronous and independent. If either of these two phases fails, both the Consumer and Provider must collaborate to resolve integration issues. In some cases, the Consumer may need to adapt its integrational code, while in others, the Provider may need to adjust its API.
After months and years of talking, trying things out, and testing, we’ve finally reached a big moment in our favorite programming language. The new Golang version, 1.18, is here. We knew it would bring significant changes to Go’s codebase, even before Generics was officially released. For a long time, when we wanted to make our code more general and abstract, we used code generators in Go. Learning what the “Go way” of doing things was challenging for many of us, but it also led to many breakthroughs. It was worth the effort. Now, there are new possibilities on the horizon.
Many new packages have emerged, giving us ideas on how we can improve the Go ecosystem with reusable code that makes life easier for all of us. This inspiration led me to create a small proof of concept using the Gorm library. Now, let’s take a look at it.
Source code # When I wrote this article, it relied on a GitHub Repository. The code served as a Go library proof of concept, with my intention to continue working on it. However, it was not yet suitable for production use, and I had no plans to offer production support at that time.
You can find the current features by following the link, and below, there is a smaller sample snippet.
Example Usage
package main import ( "github.com/ompluscator/gorm-generics" // some imports ) // Product is a domain entity type Product struct { // some fields } // ProductGorm is DTO used to map Product entity to database type ProductGorm struct { // some fields } // ToEntity respects the gorm_generics.GormModel interface func (g ProductGorm) ToEntity() Product { return Product{ // some fields } } // FromEntity respects the gorm_generics.GormModel interface func (g ProductGorm) FromEntity(product Product) interface{} { return ProductGorm{ // some fields } } func main() { db, err := gorm.Open(/* DB connection string */) // handle error err = db.AutoMigrate(ProductGorm{}) // handle error // initialize a new Repository with by providing // GORM model and Entity as type repository := gorm_generics.NewRepository[ProductGorm, Product](db) ctx := context.Background() // create new Entity product := Product{ // some fields } // send new Entity to Repository for storing err = repository.Insert(ctx, &product) // handle error fmt.Println(product) // Out: // {1 product1 100 true} single, err := repository.FindByID(ctx, product.ID) // handle error fmt.Println(single) // Out: // {1 product1 100 true} } Why have I picked ORM for PoC? # Coming from a background in software development with traditional object-oriented programming languages like Java, C#, and PHP, one of the first things I did was search Google for a suitable ORM for Golang. Please forgive my inexperience at the time, but that’s what I was expecting. It’s not that I can’t work without an ORM. It’s just that I don’t particularly like how raw MySQL queries appear in the code. All that string concatenation looks messy to me. On the other hand, I always prefer to dive right into writing business logic, with minimal time spent thinking about the underlying data storage. Sometimes, during the implementation, I change my mind and switch to different types of storage. That’s where ORMs come in handy.
Unit testing has always been my thing, almost like a hobby. There was a time when I was obsessed with it, and I made sure that all my projects had at least 90% unit test coverage. You can probably imagine how much time it can take to make such a significant change in the codebase. However, the result was worth it because I rarely encountered bugs related to business logic. Most of the issues were related to integration problems with other services or databases.
Adding new business rules was a breeze because there were already tests in place to cover all the cases from before. The key was to ensure that these tests remained successful in the end. Sometimes, I didn’t even need to check the entire running service; having the new and old unit tests pass was sufficient.
Once, while working on a personal project, I had to write unit tests to cover numerous Go structs and functions—more than 100 in total. It consumed my entire weekend, and late on a Sunday night, before heading out on a business trip the next day, I set an alarm clock to wake me up. I had hardly slept that night; it was one of those restless nights when you dream but are also aware of yourself and your surroundings. My brain was active the entire time, and in my dreams, I kept writing unit tests for my alarm clock. To my surprise, each time I executed a unit test in my dream, the alarm rang. It continued ringing throughout the night.
And yes, I almost forgot to mention, for two years, we had zero bugs in production. The application continued to fetch all the data and send all the emails every Monday. I don’t even remember my Gitlab password anymore.
Unit Testing and Mocking (in general) # In Martin Fowler’s article, we can identify two types of unit tests:
Sociable unit tests, where we test a unit while it relies on other objects in conjunction with it. For example, if we want to test the UserController, we would test it along with the UserRepository, which communicates with the database.
Solitary unit tests, where we test a unit in complete isolation. In this scenario, we would test the UserController, which interacts with a controlled, mocked UserRepository. With mocking, we can specify how it behaves without involving a database.
How often do we encounter significant changes in our preferred programming language? Some languages undergo frequent updates, while others remain traditional and stable. Go falls into the latter category, known for its consistency. “This is not the Go way!” is a phrase that often comes to mind. Most Go releases have focused on refining its existing principles. However, a major shift is on the horizon. The Go team has announced that Generics in Go are becoming a reality, moving beyond mere discussion and into implementation.
Brace yourselves, a revolution is coming.
What are Generics? # Generics allow us to parameterize types when defining interfaces, functions, and structs.
Generics is not a new concept. It has been used since the first version of Ada, through templates in C++, to its modern implementations in Java and C#. To illustrate without delving into complex definitions, let’s examine a practical example. Instead of having multiple Max or Min functions like this:
Without Generics
func MaxInt(a, b int) int { // some code } func MaxFloat64(a, b float64) float64 { // some code } func MaxByte(a, b byte) byte { // some code } we can declare now only one method, like this:
With Generics
func Max[T constraints.Ordered](a, b T) T { // some code } Wait, what just happened? Instead of defining a method for each type in Go, we utilized Generics. We used a generic type, parameter T, as an argument for the method. With this minor adjustment, we can support all orderable types. The parameter T can represent any type that satisfies the Ordered constraint (we will discuss constraints later). Initially, we need to specify what kind of type T is. Next, we determine where we want to use this parameterized type. In this case, we’ve specified that both input arguments and the output should be of type T. If we execute the method by defining T as an integer, then everything here will be an integer:
Execute Generic Function
func main() { fmt.Println(Max[int](1, 2)) } // // this code behaves exactly like method: // Max(a, b int) int And it doesn’t stop there. We can provide as many parameterized types as we need and assign them to different input and output arguments as desired:
Execute some complex Generic Function
func Do[R any, S any, T any](a R, b S) T { // some code } func main() { fmt.Println(Do[int, uint, float64](1, 2)) } // // this code behaves exactly like method: // Do(a int, b uint) float64 Here we have three parameters: R, S, and T. As we can see from the any constraint (which behaves like interface{}), those types can be, well, anything. So, up to this point, we should have a clear understanding of what generics are and how we use them in Go. Let’s now focus on more exciting consequences.