Golang Struct: The Hidden Art of OOPs Read it later

5/5 - (3 votes)

Have you ever wondered how Go, despite not having traditional classes, manages to uphold its claim of being an object-oriented programming language? It might seem perplexing at first, but fear not! Allow us to introduce you to the world of Golang struct – a remarkable feature that empowers Go to accomplish everything that classes can. By the time you finish reading this blog, any confusion surrounding the Golang struct will be dispelled, and you’ll gain a comprehensive understanding of this essential concept.

What is Struct in Golang?

In Golang, a struct is a collection of related data fields. It allows us to create custom data types by combining different data types. Similar to classes in OOP, a struct defines the structure and behavior of objects. It helps us organize data and build more maintainable programs.

Why Did Golang Choose Struct Over Class?

Golang takes a different approach by prioritizing structs over classes. Even though Golang doesn’t have traditional classes, it still allows us to implement Object-Oriented Programming (OOP) using structs in its own way. But why did Golang choose structs over classes? Let’s explore the reasons behind this decision.

  1. Simplicity and Conciseness: Using structs instead of classes, Golang avoids the complexity and overhead associated with classes and inheritance hierarchies. Structs provide a straightforward way to define custom data types without unnecessary abstractions.
  2. Composition over Inheritance: Golang promotes composition over inheritance. Instead of relying heavily on inheritance for code reuse, Golang encourages struct embedding and composition. This allows developers to combine and reuse functionality without complex inheritance trees.
  3. Flexibility and Loosely Coupled: Structs in Golang offer flexibility and loose coupling between components. Golang separates data fields (defined in structs) from the methods that operate on them. This separation provides greater flexibility, allowing methods to be attached to multiple struct types and promoting code reuse with decoupled data and behavior.
  4. Performance and Memory Efficiency: Golang prioritizes performance and memory efficiency. Structs in Golang are value types, meaning they are passed by value rather than by reference. This results in better performance and reduced memory overhead, especially with small and lightweight data structures. The absence of class-level operations and inheritance hierarchies further contributes to efficient Golang programs.
  5. Focus on Interfaces: Golang places emphasis on interfaces for abstraction. Instead of relying primarily on classes, Golang focuses on interfaces, which define behavior without specifying the underlying implementation. Structs in Golang can implicitly satisfy interfaces, enabling runtime polymorphism and dynamic dispatch, essential principles of OOP.

Defining Golang Struct

Now that we’ve discussed what are structs and why Golang prefers them over classes, let’s dive into the process of defining a struct. Understanding how to define a struct is a fundamental step towards mastering its capabilities. Don’t worry, it’s not as daunting as it may sound, in fact, it’s quite fascinating!

Struct Syntax

In Golang, a struct is defined using the combination of the type and struct keywords. This simple syntax allows us to create custom data types with their own set of fields, enabling us to organize related data in a structured manner. Here’s an example of a basic struct definition:

type StructName struct {
    Field1 string
    Field2 int
    Field3 float64
}

In the above code snippet, we define a struct named StructName with three fields: Field1 of type string, Field2 of type int, and Field3 of type float64. Each field represents a specific aspect of the struct, allowing us to store and access different types of data within a single entity.

Multiple Struct Declaration

But wait, there’s more! Golang also allows us to define multiple structs within a single declaration, which can be quite handy when we need to work with different struct types simultaneously. Let’s take a look at an example:

type (
    Struct1 struct {
   	Field1 int
	Field2 float64
    }

    Struct2 struct {
	Field1 bool
	Field2 string
    }

    Struct3 struct {
	Field1 []int
	Field2 string
    }
)

In the above example, we define three different structs: Struct1, Struct2, and Struct3. Each struct has its own set of fields, allowing us to represent distinct types of data structures. This flexibility in struct definitions empowers us to model our data in the most appropriate and meaningful way.

These methods of defining structs in Golang are referred to as Named Structs, as it involves assigning a name to each of the defined structs.

Golang Struct Initialization

In the previous section, we discussed how to define a struct in Golang. Now, let’s dive into the crucial step of initializing a struct. There are three primary ways to initialize a struct in Golang:

  1. using struct literal,
  2. using the pointer address operator &, and
  3. using the new keyword.

We will explore each of these methods and provide a comparative analysis by considering an example struct.

Let’s consider the following example struct to demonstrate the initialization methods:

type Student struct {
    Name       string
    RollNumber int
    TotalMarks float64
}

1. Using Struct Literal

The struct literal approach is the simplest way to initialize a struct. You assign values directly to the fields of the struct.

Example:

var s Student = Student{
    Name:       "Divyanshu Shekhar",
    RollNumber: 17,
    TotalMarks: 99,
}
// OR
// var s = Student{"Divyanshu Shekhar", 17, 99}
// OR
// s := Student{"Divyanshu Shekhar", 17, 99}
fmt.Println(s)

Output:

{Divyanshu Shekhar 17 99}

However, what if we don’t have all the data at hand? Can we still initialize the struct? Absolutely! We can initialize an empty struct and assign values to its fields later using the dot . notation.

Example:

s := Student{}
s.Name = "Divyanshu Shekhar"
s.RollNumber = 17
s.TotalMarks = 99

2. Using Pointer Address Operator

The second method involves using the pointer address operator & to initialize a struct. This approach returns a reference to the initialized struct object instead of a copy.

Example:

var s *Student = &Student{
    Name:       "Divyanshu Shekhar",
    RollNumber: 17,
    TotalMarks: 99,
}
// OR
// var s = &Student{"Divyanshu Shekhar", 17, 99}
// OR
// s := &Student{"Divyanshu Shekhar", 17, 99}

To access the value at the address, we need to dereference the pointer.

Output:

{Divyanshu Shekhar 17 99}

Similar to the previous approach, you can initialize an empty struct and assign values to its fields afterward.

Example:

var s *Student = &Student{}
s.Name = "Divyanshu Shekhar"
(*s).RollNumber = 17
s.TotalMarks = 99.8

3. Using the New Keyword

When working with large structs, using the pointer address operator is often preferred for memory efficiency. However, developers may sometimes find the syntax of the address operator confusing. To address this, the Golang team introduced the new keyword for struct initialization.

Example:

var s *Student = new(Student)
s.Name = "Divyanshu Shekhar"
s.RollNumber = 17
s.TotalMarks = 98.9

This approach provides a more straightforward syntax, making it easier for developers to work with structs.

Accessing Struct Fields in Go

When it comes to accessing fields within a Golang struct, it’s as simple as using the dot (.) notation. Just like how we inserted data into each field using dot notation, we can fetch the values in a similar way. Let’s take a look at an example:

fmt.Println("Name: ", s.Name)               // Divyanshu Shekhar
fmt.Println("Roll Number: ", s.RollNumber)  // 17
fmt.Println("Total Marks: ", s.TotalMarks)  // 99

In the above code snippet, we assume that we have a struct instance called s with fields such as Name, RollNumber, and TotalMarks. By using the dot notation and the name of the field, we can access the corresponding values. In this case, we retrieve the name, roll number, and total marks from the s struct and print them to the console.

Struct as Argument in Function

When it comes to passing a struct as an argument in a function, we have two approaches:

  1. Pass by value, and
  2. Pass by reference.

Pass By Value

In this method, a copy of the struct object is passed to the function. Any changes made within the function only affect the copy, and the original object remains unchanged.

For example, let’s say we have a function called NameToLowerCase that aims to convert the name field of a Student struct to lowercase:

func NameToLowerCase(s Student) {
    s.Name = strings.ToLower(s.Name)
}

func main() {
    var s Student = Student{
	Name:       "Divyanshu Shekhar",
	RollNumber: 17,
	TotalMarks: 98.9,
    }
    NameToLowerCase(s)
    fmt.Println(s.Name) // Output: Divyanshu Shekhar
}

As you can observe from the output, the name field’s value remains unchanged even after calling the NameToLowerCase function. This is because the function operates on a copy of the struct passed by value.

Pass by Reference

In contrast, pass-by-reference involves passing the address of the struct object (a pointer) as the function argument. By doing so, any modifications made within the function directly affect the original object itself.

Let’s modify our example to demonstrate pass-by-reference:

func NameToLowerCase(s *Student) {
    s.Name = strings.ToLower(s.Name)
}

func main() {
    var s Student = Student{
	Name:       "Divyanshu Shekhar",
	RollNumber: 17,
	TotalMarks: 98.9,
    }
    NameToLowerCase(&s)
    fmt.Println(s.Name) // Output: divyanshu shekhar
}

In this updated version, we pass the address of the Student object (&s) to the NameToLowerCase function. As a result, the function is able to modify the original object, and the name field is converted to lowercase.

Golang Struct Methods: The OOPs Moment

This is what we’ve been eagerly waiting for, right? It’s time to explore how Golang Structs can be used as an Object-Oriented Programming (OOP) paradigm.

Let’s embark on a little imagination journey together. Imagine a struct as a class where you define the fields. So far, so good, isn’t it? However, we haven’t written any methods within the struct to perform operations on those fields. But what if we could attach methods to the struct? Well, that’s when we truly create the essence of OOPs in Go. And here’s the exciting part: Golang provides a unique way to attach methods to structs. Let’s delve into it!

By now, you might be familiar with the syntax of functions where you’ve seen parentheses after the function names. But when it comes to attaching a method to a struct, you’ll see a slight variation. Brace yourself, as I reveal the syntax to you:

func (objName StructName) FunctionName(<parameters>) {}

That’s right, the parentheses are placed before the function name! This is what allows us to attach the method to the struct.

Now, when defining methods, there are two approaches you can take: pass-by-value and pass-by-reference. If you’re not yet familiar with these concepts, I recommend referring to the previous section for a detailed explanation.

However, if you already have a solid understanding, let’s focus on how to attach your struct method to an object using pass-by-reference. Simply follow this syntax:

func (objName *StructName) FunctionName(<parameters>) {}

It’s as simple as placing an asterisk before the struct type name. This indicates that we are working with a reference to the object rather than a copy.

Golang OOPs Example

In the previous section, we explored the syntax of struct methods. Now, let’s delve into a practical example to solidify our understanding.

Imagine we want to work with rectangles in our Go program. We’ll define a Rectangle struct with two fields: length and breadth. To make things more convenient, we’ll also create a constructor function that initializes the struct fields with the provided length and breadth values. Additionally, we’ll implement an Area function that calculates and returns the area of the rectangle.

Here’s the code for our example:

package main

import "fmt"

type Rectangle struct {
	length, breadth int
}

// Constructor
func (r *Rectangle) Init(l, b int) *Rectangle {
	return &Rectangle{l, b}
}

// Area Function
func (r Rectangle) Area() int {
	return r.length * r.breadth
}

func main() {
	r := new(Rectangle).Init(10, 20)
	fmt.Println("Rectangle is: ", r) // Output: &{10,20}
	fmt.Println("Rectangle area is: ", r.Area()) // Output: 200
}

In the code snippet above, we define the Rectangle struct with its length and breadth fields. The constructor function Init takes in the length and breadth as arguments and returns a pointer to a new Rectangle instance with the fields properly initialized.

The Area function, defined on the Rectangle struct, calculates the area by multiplying the length and breadth. It returns the resulting area as an integer value.

In the main function, we create a new Rectangle instance using the constructor function. We pass in the length as 10 and the breadth as 20. Then, we print out the rectangle and its calculated area using the fmt.Println function.

Embedding Structs in Go

Struct embedding is a powerful feature in Go that allows you to compose structs inside other structs, enabling seamless composition of types. This feature provides a convenient way to combine the fields and behaviors of multiple structs into a single composite struct. There are two approaches to struct embedding: anonymous embedded fields and explicit fields.

Let’s explore these two approaches using an example scenario. Consider two structs: Email and ContactMe. We want to embed the Email struct into the ContactMe struct. Let’s see how it can be done using both approaches:

1. Anonymous Embedded Fields

In anonymous embedding, we don’t provide a field name during embedding. Instead, the embedded type itself is used as the field name during initialization.

Here’s an example code snippet demonstrating anonymous embedding:

type Email struct {
    from, to, subject, body string
}

type ContactMe struct {
    Email
    phone string
}

func main() {
    contact := ContactMe{
	Email: Email{
        	from:    "hackthedeveloper@gmail.com",
		to:      "info@gmail.com",
		subject: "Hello",
		body:    "Hello, How are you?",
	},
	phone: "1234567890",
    }

    fmt.Println(contact)
}

In the above example, we define the Email struct with its fields. Then, we create the ContactMe struct, embedding the Email struct using the anonymous field syntax. During initialization, we assign values to the fields of the embedded Email struct using the Email field name. The anonymous embedding allows us to access the fields of the embedded struct directly through the outer struct.

2. Explicit Field Embedding

In explicit field embedding, we provide an explicit field name along with the embedding type. This approach gives more control over the naming of the embedded field.

Here’s an example demonstrating explicit field embedding:

type Email struct {
    from, to, subject, body string
}

type ContactMe struct {
    email Email
    phone string
}

func main() {
    contact := ContactMe{
	email: Email{
		from:    "hackthedeveloper@gmail.com",
		to:      "info@gmail.com",
		subject: "Hello",
		body:    "Hello, How are you?",
	},
	phone: "1234567890",
    }

    fmt.Println(contact)
}

In the above code, we define the Email struct with its fields. Then, we create the ContactMe struct, embedding the Email struct using the explicit field name email. During initialization, we assign values to the fields of the embedded Email struct using the email field name. This approach provides more clarity and control when accessing the embedded struct fields.

Using Embedded Struct Method

When we use struct embedding, we not only inherit the fields of the embedded struct but also its methods. This allows us to conveniently access and use the methods associated with the embedded struct through the parent struct.

Let’s refer back to our previous example of the ContactMe struct, where we embedded the Email struct. Now, let’s define a method for the Email struct and see how we can call it using an object of the ContactMe struct.

type Email struct {
	from, to, subject, body string
}

func (e Email) send() {
	fmt.Println("Email sent to", e.to)
}

type ContactMe struct {
	email Email
	phone string
}

func main() {
	contact := ContactMe{
		email: Email{
			from:    "hackthedeveloper@gmail.com",
			to:      "info@gmail.com",
			subject: "Hello",
			body:    "Hello, How are you?",
		},
		phone: "1234567890",
	}

	fmt.Println(contact)
	contact.email.send() // Output: Email sent to info@gmail.com
}

In the above code snippet, we define the Email struct with a send() method that prints a message indicating that an email has been sent to the recipient. The ContactMe struct embeds the Email struct as a field.

Inside the main() function, we create an instance of the ContactMe struct, initializing its fields, including the embedded Email struct. We then print the contact object to verify its contents.

To call the send() method of the embedded Email struct, we use the dot notation (contact.email.send()). This allows us to access the send() method directly through the parent struct, providing the functionality to send an email to the specified recipient.

Generic Struct in Golang

Did you know that Golang introduced support for generic functions and types starting from version 1.18? It’s an exciting addition to the language, and we’re here to guide you through it.

With the introduction of generics, Go now allows us to create generic types by adding a new syntax of type parameters after the struct name. This enables us to define structs that can hold data of different types, making them more flexible and reusable.

Let’s illustrate this concept with an example. Imagine we want to create a Student struct where the Marks field can accept either an integer or a floating-point number. To make the struct generic, we can use the following syntax:

type Student[T int | float64] struct {
    Name  string
    Marks T
}

func main() {
    s1 := Student[int]{
	Name:  "John",
	Marks: 90,
    }

    s2 := Student[float64]{
	Name:  "John",
 	Marks: 90.5,
    }

    fmt.Println(s1.Name, s1.Marks)
    fmt.Println(s2.Name, s2.Marks)
}

In the above code, we define a Student struct with a type parameter T that can be either an int or a float64. By using this generic struct, we can create instances of Student with different types of Marks. As you can see, we initialize s1 with an integer value and s2 with a floating-point value.

By embracing generics in Go, we can now create more versatile and reusable code. It opens up possibilities for building data structures, algorithms, and functions that can work with various types, reducing the need for code duplication.

If you want to explore the topic of generics in Golang in more detail, we recommend checking out our blog post on Golang Generics. It provides comprehensive insights and examples to help you dive deeper into this exciting feature.

Wrapping Up

In conclusion, Golang structs are a powerful tool for organizing and manipulating data in a structured manner. From basic concepts to advanced techniques, we’ve explored the ins and outs of structs, including initialization, accessing fields, and utilizing methods. We’ve also covered advanced topics like concurrency, performance optimization, and struct patterns. By mastering structs, you’ll enhance your code organization and create efficient solutions. Happy coding!

Frequently Asked Questions (FAQs)

What is a struct in Golang?

In Golang, a struct is a composite data type that allows you to define a blueprint for creating objects with specific properties. It encapsulates a group of related values and provides a structured way to organize data.

How to create a struct in Golang?

To create a struct in Golang, you use the type keyword followed by the struct’s name and a set of field declarations within curly braces. Each field declaration consists of a name and a type. This defines the structure of the struct.

What are struct tags in Golang?

Struct tags are annotations associated with struct fields in Golang. They are used to provide metadata or additional information about the fields. Struct tags are commonly used for purposes like serialization, validation, or mapping to other formats.

Can I embed one struct within another in Golang?

Yes, Golang supports struct embedding, allowing you to include one or more structs within another struct. This composition mechanism promotes code reusability and helps build more complex data structures.

Are structs in Golang thread-safe?

By default, structs in Golang are not thread-safe. If multiple goroutines access and modify the same struct concurrently, proper synchronization mechanisms like mutexes or atomic operations need to be implemented to ensure thread safety.

Reference

Was This Article Helpful?

Leave a Reply

Your email address will not be published. Required fields are marked *