- read

Mastering Golang Basics: A Deep Dive into Structs, Interfaces, Embedded Structs, and Generics…

Sudhanva MG 72

Introduction

Welcome to a foundational journey into the heart of Golang, commonly known as Go. While the internet teems with resources about this increasingly popular language, this guide aims to tailor insights specifically for backend developers looking to find their footing.

As we navigate through the landscapes of backend development, it’s crucial to highlight why Go often stands out as a preferred choice for many developers.

Strongly Typed Nature: One of Go’s most defining features is its statically typed nature. Every variable, every function, and every structure has a definitive type. This not only enforces discipline while coding but also offers a layer of predictability, making it easier for developers to write, debug, and even read others’ code.

Concurrency at its Core: Go’s in-built goroutines and channels make concurrent programming a breeze, allowing backend applications to achieve efficient parallelism without the complexities that other languages might introduce.

Simplified Dependency Management: With modules, Go offers a clean and integrated mechanism for dependency management, making it straightforward for developers to maintain and scale projects.

Performance Efficiency: Go’s design is inherently geared towards performance, with a fast compilation time and the benefits of garbage collection, offering a blend of both dynamic and compiled languages.

Minimalist and Clean Syntax: The language’s simplicity doesn’t just make it easy to learn; it ensures that codes remain clean and maintainable, a boon for extensive backend projects.

Through the course of this blog, we’ll demystify core concepts like:

  • Packages & Encapsulation: The building blocks of any Go application.
  • Structs & Embedded Structs: Blueprinting data and fostering inheritance.
  • Interfaces: Polymorphism’s Go counterpart, fostering versatile designs.
  • Generics: A dive into type-safe data structures and functions.

For seasoned Golang aficionados, this read might seem like revisiting familiar terrains. However, if you’re just dipping your toes into the vast sea of Go, or transitioning from another backend language, this guide aims to bridge the gap. Drawing inspiration from renowned resources like Go by Example, we’ll strive to deliver a blend of foundational knowledge, enriched with backend-centric examples.

Building with Purpose: Crafting a Simple CMS System with Go

Diving into a new language or framework is always more intuitive when there’s a tangible project to wrap our heads around. For our Golang exploration, we’ll be constructing a basic Content Management System (CMS). This CMS will not only help solidify the fundamental concepts we discuss but also give you hands-on experience with real-world applications.

Here’s a glimpse of what our CMS will be capable of:

  1. Content Creation: Differentiate and craft varied types of content.
  2. Memory Storage: Efficiently store the content within an in-memory data structure.
  3. Content Retrieval: Fetch specific content pieces as and when required.
  4. Update Mechanisms: Modify existing content, reflecting changes in real-time.

Before we embark on our Golang adventure, it’s imperative to ensure we have the primary tool at our disposal — the Go language itself. If you haven’t already set up Go on your system, let’s walk through the simple process.

Visit the Official Go Downloads Page: Navigate to Go’s official downloads page. Here, you’ll find installation packages for various operating systems including Windows, macOS, and Linux.

Download the Installer: Choose the appropriate installer for your operating system and download it. For most users, the default installation settings will suffice.

Installation:

  • For Windows users, run the downloaded .msi file and follow the prompts.
  • macOS users can open the downloaded .pkg file and it will guide you through.
  • For Linux aficionados, extract the downloaded archive to /usr/local using the command tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz, replacing $VERSION, $OS, and $ARCH with your respective downloaded version details.

Setting up Environment Variables (optional, but recommended): After installation, you might want to set up your GOPATH, which is the location where your Go projects will reside. Though recent versions of Go set up a default GOPATH, having it configured explicitly can give you better control.

Verify Installation: Finally, open up your terminal or command prompt and type:

go version

If you see the Go version you installed, congratulations! You’ve successfully set up Go on your machine. Now, with Go firmly in place, let’s jump into initializing our CMS project and diving deeper into the world of Golang.

A Quick Note on Our Approach

Before we dive deeper, I’d like to emphasize an important point. The structuring and organization of the code in this blog post have been designed primarily for clarity and educational purposes. While it offers a way to understand the core concepts of Go, especially for those new to the language, it might not necessarily represent the best practices for creating production-ready applications. There are several ways to organize Go code, and what’s presented here aims to make the learning curve smoother. Always consult more comprehensive resources and best practices when designing systems for real-world use cases.

Kickstarting Our Golang Journey: Setting up the CMS Project

Create a Project Folder: To keep our project organized, we start by creating a dedicated folder.

mkdir cms
cd cms

Initialize the Go Project: Here comes the critical part!

go mod init github.com/username/cms

Now, let’s decipher this command:

  • go mod init: This command initializes a new module, creating a go.mod file. The go.mod file is where Go manages the project's dependencies. Modules are a collection of related Go packages stored in a file tree with a go.mod file at the root. They're essentially the Go way of defining a project, ensuring versions and dependencies are handled efficiently.
  • github.com/username/cms: This part defines the module path. It's customary (though not mandatory) to use a GitHub URL structure because it provides a global uniqueness to your module path. The reason for this is historical and rooted in Go's early days, where importing packages directly from version control repositories, like GitHub, was a prevalent practice. By sticking to this convention, it ensures that if you ever publish your package, the path remains consistent, and there won't be any naming collisions.

To sum it up, with the go mod init command, we're essentially telling Go: "Hey, I'm starting a new project, and here's its unique identifier in the grand ecosystem of Go projects." This ensures that as you pull in dependencies or if others use your module, everything remains harmonious and collision-free.

Structuring Golang Backend Projects: Embracing Best Practices

The structure of a Go project plays a vital role in ensuring clarity, maintainability, and scalability. While Go doesn’t strictly enforce a particular project structure, the community has, over time, identified a set of best practices that facilitate collaboration and readability. Let’s delve into the recommended ways to structure our Golang backend projects:

  1. Flat is Better than Nested: Unlike some other languages, Go tends to favour a flatter directory structure. This doesn’t mean you should avoid sub-directories, but rather that you should create them thoughtfully.
  2. Naming Conventions: Name directories after the package they provide. Go encourages short, concise package names, avoiding plurals, and steering clear of generic names like models or utilities.
  3. Project Root Directory: This will contain the main entry point for your application, the main.go file. It's also where you'll have the go.mod and go.sum files, which handle dependencies.
  4. /cmd: For larger applications with multiple binaries, you’d have a /cmd directory, which contains subdirectories named for each binary the project will produce.
  5. /content: Here, you can store the main logic or business rules of your application. For our CMS, this might entail logic related to creating, updating, or deleting content.
  6. /store: Any logic related to data storage or retrieval can be housed here. This could interface with databases, caches, or other data storage mechanisms.
  7. /application: This would handle the higher-level application logic. It might manage things like application configuration, routing, and middleware.
  8. /api: If your backend exposes a RESTful API, you might have a /api directory. It would contain route definitions, request handlers, and anything directly associated with the API's interface.
  9. /pkg (optional): If there are parts of your codebase that you believe could be useful as standalone packages or for other projects, you’d place them under a /pkg directory. This signals that the code inside this folder is safe for use by others.
  10. /internal: Code you don’t want to be accessible to other applications should be placed in a /internal directory. Go will prevent any external code from importing code from directories named internal.
  11. /test: All your test files, mock data, and test-related utilities can find a home here.
  12. Avoid Package-Level Globals: While it can be tempting to use package-level variables for things like database connections, it can lead to unpredictable behaviour due to shared state. Instead, prefer to pass such shared resources explicitly.

Remember, these are best practices and guidelines. Depending on the project’s needs and its size, you might decide to adopt a slightly different structure. The critical part is to ensure that the project remains organized, scalable, and easy to navigate, both for you and any future contributors.

With a solid understanding of how to effectively structure our Go backend, we’re set to start building our CMS system in earnest.

Alright, now that we have a solid understanding of the best practices and guidelines, let’s set up the structure for our CMS project. Follow these steps to establish the foundational directories and files:

Creating the content Directory: This directory will house the logic related to different content types and associated methods.

mkdir content

Inside this directory, we’ll later add files like image.go, video.go, etc., each defining a different content type and its associated methods.

Creating the store Directory: This will serve as our in-memory content store. Here, we'll manage functions related to storing, fetching, updating, and deleting content.

mkdir store

Within the store directory, we might have a file called inmemory.go that provides an in-memory representation of our content storage and associated CRUD operations.

Creating the main.go File: This will be the entry point for our application. It will help set up the application, route requests, and manage high-level logic.

touch main.go

Inside main.go, we will initialize our in-memory store, set up any necessary middleware, and define the main functions that our CMS system will use.

Understanding Structs and Defining Content Types

Structs are a way to define custom data types in Go, which allows us to combine data of different types, group them together, and use them to represent more complex data structures. Think of a struct as a blueprint that can be used to create objects. In the context of our CMS, we will use structs to represent different content types, thereby allowing us to model and work with our data more effectively.

In many object-oriented languages, you might have classes that contain both data and methods. In Go, structs are about data, while associated methods can be defined separately using receivers.

Here’s a simple breakdown:

  • Fields: Each variable inside the struct is called a field. For example, Title and ID are fields in our content structs.
  • Tagging: The syntax using backticks (json:"title") is known as a field tag. It's a string of metadata associated with the field at compile time. In our case, we are using JSON tags to instruct the JSON package how to encode or decode the struct fields to/from JSON.

With the foundational understanding in place, let’s define the content types:

ImageContent: Represents content that is primarily an image. It has a title, a unique ID, and a URL pointing to the image location.

type ImageContent struct {
Title string `json:"title"`
ID string `json:"id"`
ImageUrl string `json:"image_url"`
}

VideoContent: Similar to ImageContent but instead of an image, it points to a video resource.

type VideoContent struct {
Title string `json:"title"`
ID string `json:"id"`
VideoUrl string `json:"video_url"`
}

TextContent: Represents content that is mainly textual. It also has a title, a unique ID, and the main body of text.

type TextContent struct {
Title string `json:"title"`
ID string `json:"id"`
Text string `json:"text"`
}

By structuring our content types this way, we ensure that our system is both organized and scalable. Later, if we decide to add more fields or properties to a particular content type, we can easily do so by modifying the respective struct.

Struct Embedding: Simplifying and Enhancing Go Structs

The Go programming language provides an interesting feature known as struct embedding. It allows developers to embed one struct inside another. This means that the embedded struct’s fields can be accessed as if they were fields of the outer struct, providing a way to reuse common parts of multiple structs. This is a nifty way to implement a form of inheritance and composition in Go, which doesn’t support classic OOP inheritance.

Let’s break this down:

  • Why use struct embedding? In our CMS example, as you noticed, every content type (Image, Video, and Text) has common metadata, namely the Title and ID. By embedding, we don't have to repeat these fields in every struct, thus keeping our code DRY (Don't Repeat Yourself) and more maintainable.
  • How does it work? Instead of the traditional field name followed by its type, you simply specify the type (the struct you want to embed). This embedded struct’s fields then get promoted to the outer struct, meaning you can access them directly.

For our CMS system:

// Metadata encapsulates the common properties every content type has.
type Metadata struct {
Title string `json:"title"`
ID string `json:"id"`
}
// ImageContent now has Title, ID, and ImageUrl fields directly accessible.
type ImageContent struct {
Metadata // Embedding the Metadata struct
ImageUrl string `json:"image_url"`
}
// VideoContent now has Title, ID, and VideoUrl fields directly accessible.
type VideoContent struct {
Metadata // Embedding the Metadata struct
VideoUrl string `json:"video_url"`
}
// TextContent now has Title, ID, and Text fields directly accessible.
type TextContent struct {
Metadata // Embedding the Metadata struct
Text string `json:"text"`
}

With this approach, creating an ImageContent object with a title and ID is as straightforward as before, but the struct definition itself is more streamlined and reusable. If at some point we wish to add more common fields to every content type, we can just update the Metadata struct without having to touch each individual content type struct.

However, one thing to keep in mind with struct embedding is the potential for field clashes. If the outer struct and the embedded struct have fields with the same name, the outer field takes precedence.

Harnessing the Power of Interfaces in Go

Interfaces are central to Go’s type system. They allow us to define behaviour and let multiple types satisfy that behaviour without strictly binding the method to the type. This provides a high level of flexibility and adaptability in our code.

Let’s break down the Content interface we just introduced:

type Content interface {
GetID() string
GetType() string
}

This interface declares that any type that wishes to be recognized as Content needs to have methods GetID and GetType. It doesn’t matter how the type is structured or what other methods it has, as long as it has these two methods, it can be treated as Content.

Now, let’s implement this interface for our content types:

func (c TextContent) GetID() string {
return c.ID
}
func (c TextContent) GetType() string {
return "Text"
}
func (c ImageContent) GetID() string {
return c.ID
}
func (c ImageContent) GetType() string {
return "Image"
}
func (c VideoContent) GetID() string {
return c.ID
}
func (c VideoContent) GetType() string {
return "Video"
}

By implementing the Content interface in our content types, we can now write a function that takes in a Content and processes it, regardless of whether it's text, image, or video. Here's a simple example:

func ProcessContent(c Content) {
fmt.Printf("Processing content of type: %s with ID: %s\n", c.GetType(), c.GetID())
}

Now, you can call this function with any content type:

text := TextContent{Metadata{"1", "Sample Text"}, "This is a sample text."}
ProcessContent(text)
image := ImageContent{Metadata{"2", "Sample Image"}, "https://sampleimageurl.com"}
ProcessContent(image)

This showcases how interfaces allow us to write functions that are generic and can work with multiple types, promoting code reusability and scalability. As you add more content types in the future, as long as they satisfy the Content interface, they can be easily integrated into existing processes without any major code changes.

Building a Robust Store with Interfaces

As we proceed, we’ll understand that while Go is not an object-oriented language in a traditional sense, it indeed supports robust design principles that can provide modularity and extensibility to our system. One of those principles is to “Accept interfaces, return structs”. By defining our store operations as an interface, we’re paving a path for flexibility, allowing various store implementations in the future if needed.

Storer is our foundational interface for storing content. It ensures uniformity in how we interact with the underlying storage, whether it's in memory, a database, or something else entirely.

type Storer interface {
Create(content content.Content) error
Update(content content.Content, id string) error
Fetch(id string) (content.Content, error)
}

For the sake of simplicity, our example will utilize an in-memory store, specifically for text content. However, this approach can easily be expanded for other content types or more advanced storage mechanisms.

Let’s break down our TextStore:

type TextStore struct {
data map[string]content.TextContent
}
func NewTextStore() *TextStore {
return &TextStore{
data: make(map[string]content.TextContent),
}
}

Our TextStore struct uses a simple map to hold the data, with the content ID as the key and the TextContent itself as the value.

The Create function, as its name suggests, allows us to store a new piece of content:

func (t *TextStore) Create(content content.Content) error {
textContent, ok := content.(content.TextContent)
if !ok {
return fmt.Errorf("Invalid type, expected TextContent")
}
t.data[textContent.GetID()] = textContent
return nil
}

In the function, we try to type assert our interface type (content.Content) to our struct type (content.TextContent). If the type assertion is successful, we can safely add the content to our store.

Now, let’s implement the Update function:

func (t *TextStore) Update(content content.Content, id string) error {
textContent, ok := content.(content.TextContent)
if !ok {
return fmt.Errorf("Invalid type, expected TextContent")
}
_, exists := t.data[id]
if !exists {
return fmt.Errorf("Content with ID %s not found", id)
}
t.data[id] = textContent
return nil
}

Finally, the Fetch function:

func (t *TextStore) Fetch(id string) (content.Content, error) {
textContent, exists := t.data[id]
if !exists {
return nil, fmt.Errorf("Content with ID %s not found", id)
}
return textContent, nil
}

The Update and Fetch methods similarly interact with our in-memory map, providing the necessary CRUD functionality for our content.

Embracing Generics in Go: A Game-Changer

Generics have been a long-awaited feature in the Go language. Unlike many other programming languages where generics have been around for quite some time, Go took a conservative approach to adding this feature, wanting to ensure it complements the language’s simplicity and efficiency. With the introduction of generics, Go developers can now write more flexible and reusable code without compromising on type safety.

What are Generics?

At a high level, generics allow developers to write functions and data structures that operate on a specified type without determining that type upfront. This means you can write a function or a struct once and use it with multiple different types, as long as they adhere to the requirements you’ve set.

To visualize this, consider how you might implement a function to swap two integers. Now, what if you wanted to swap two strings? Or two floating-point numbers? Without generics, you’d have to write a new function for each of these data types. With generics, you can write a single, generalized function and use it for any type.

Back to our CMS scenario, instead of creating separate stores for each content type (TextStore, ImageStore, VideoStore), wouldn't it be efficient to have a single store that can handle any content type?

Using Generics to Create a Universal Store

With the introduction of generics, we can design our store to be type-agnostic. Here’s how it can be done:

type Store[T content.Content] struct {
data map[string]T
}

func NewStore[T content.Content]() *Store[T] {
return &Store[T]{
data: make(map[string]T),
}
}

func (s *Store[T]) Create(content T) error {
s.data[content.GetID()] = content
return nil
}

func (s *Store[T]) Update(content T, id string) error {
existingContent, exists := s.data[id]
if !exists {
return fmt.Errorf("Content with ID %s not found", id)
}
s.data[id] = content
return nil
}

func (s *Store[T]) Fetch(id string) (T, error) {
content, ok := s.data[id]
if !ok {
return nil, fmt.Errorf("Content with ID %s not found", id)
}
return content, nil
}

With the generic store, we use the square brackets [] to define our generic type T, which in this case, must implement the content.Content interface. We've just created a universal store where the type is deferred until usage. This means you can instantiate a store for text content, video content, or any other content type with the same code.

In Conclusion

Generics are a powerful feature that can reduce code redundancy and increase type safety. They bridge the gap between flexibility and type safety, allowing for more modular and scalable design. While they can be a bit intimidating for beginners, understanding and embracing them can lead to more efficient and cleaner code in Go.

Putting It All Together: Crafting the Main Application

Having structured our backend system, built various content types, and created flexible stores to manage them, it’s now time to piece everything together. We’ll create our main application entry point where we’ll instantiate the stores, add some content, and showcase how our system works.

package main

import (
"fmt"
"github.com/username/cms/content"
"github.com/username/cms/store"
"math/rand"
"time"
)

func generateID() string {
rand.Seed(time.Now().UnixNano())
return fmt.Sprintf("%d", rand.Intn(1000))
}

func main() {
// Create Stores
textStore := store.NewStore[content.TextContent]()
imageStore := store.NewStore[content.ImageContent]()
videoStore := store.NewStore[content.VideoContent]()

// Create and Add Text Content
text := content.TextContent{
Metadata: content.Metadata{ID: generateID(), Title: "Sample Text"},
Text: "This is a sample text content.",
}
textStore.Create(text)

// Create and Add Image Content
image := content.ImageContent{
Metadata: content.Metadata{ID: generateID(), Title: "Sample Image"},
ImageUrl: "http://example.com/image.jpg",
}
imageStore.Create(image)

// Create and Add Video Content
video := content.VideoContent{
Metadata: content.Metadata{ID: generateID(), Title: "Sample Video"},
VideoUrl: "http://example.com/video.mp4",
}
videoStore.Create(video)

// Fetch and Print Content
fetchedText, err := textStore.Fetch(text.ID)
if err == nil {
fmt.Println(fetchedText)
}

fetchedImage, err := imageStore.Fetch(image.ID)
if err == nil {
fmt.Println(fetchedImage)
}

fetchedVideo, err := videoStore.Fetch(video.ID)
if err == nil {
fmt.Println(fetchedVideo)
}
}
  1. Imported necessary packages: our content and store packages, along with a few standard Go libraries.
  2. Defined a helper function generateID to produce unique IDs for our content.
  3. Instantiated our content stores.
  4. Created sample content for text, image, and video, and added them to their respective stores.
  5. Fetched and printed the contents to the console.

By running go run main.go, you'll be able to see the entire process in action, from creating content types to storing and fetching them. This is a simple example, but the architecture allows for easy expansion. You can add more functionalities, like deleting content, implementing more complex querying capabilities, or even integrating with external databases. The modular structure we've adopted will make these additions seamless.

Conclusion

Embarking on the journey to understand the Go programming language — especially from the perspective of backend development — can be a thrilling experience. As we’ve delved into the depths of Go’s capabilities, we’ve seen how its strong typing, interfaces, and recently introduced generics can elegantly solve real-world challenges.

Through our simple Content Management System (CMS), we’ve demonstrated how Go facilitates clean, scalable, and efficient code structures. By embracing Go’s paradigms, we ensure our systems are not only performant but also maintainable. The modular architecture adopted in our CMS can be a blueprint for many more complex applications, and it underscores the versatility of Go.

For backend developers, Go offers a blend of simplicity and power. It’s a language that doesn’t bog you down with verbose syntax but gives you the tools to build robust systems. While our CMS is just a tip of the iceberg, it provides a foundation upon which many other functionalities can be integrated.

Whether you’re a beginner just starting with Go or an experienced developer looking to refine your skills, always remember that the true power of a programming language comes from understanding its core principles and leveraging them effectively. And in that realm, Go has plenty to offer.

Until our next deep dive, happy coding!