Category: Go Programming Language

  • File Handling in GO

    Reading Files

    1. Reading an Entire File into Memory

    The simplest file operation is reading an entire file into memory using the os.ReadFile function. Below is an example.

    Directory Structure

    ├── Workspace
    │   └── filedemo
    │       ├── main.go
    │       ├── go.mod
    │       └── sample.txt

    Content of sample.txt:

    Welcome to Go file handling!

    Code in main.go:

    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    func main() {
    	content, err := os.ReadFile("sample.txt")
    	if err != nil {
    		fmt.Println("Error reading file:", err)
    		return
    	}
    	fmt.Println("File content:", string(content))
    }

    Run Instructions:

    cd ~/Workspace/filedemo/
    go install
    filedemo

    Output:

    File content: Welcome to Go file handling!

    If you run the program from a different directory, you’ll encounter:

    Error reading file: open sample.txt: no such file or directory
    2. Using an Absolute File Path

    Using an absolute path ensures the program works regardless of the current directory.

    Updated Code in main.go:

    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    func main() {
    	content, err := os.ReadFile("/Users/user/Workspace/filedemo/sample.txt")
    	if err != nil {
    		fmt.Println("Error reading file:", err)
    		return
    	}
    	fmt.Println("File content:", string(content))
    }

    Output:

    File content: Welcome to Go file handling!
    3. Passing the File Path as a Command-Line Argument

    Using the flag package, we can pass the file path dynamically.

    Code in main.go:

    package main
    
    import (
    	"flag"
    	"fmt"
    	"os"
    )
    
    func main() {
    	filePath := flag.String("file", "sample.txt", "Path of the file to read")
    	flag.Parse()
    
    	content, err := os.ReadFile(*filePath)
    	if err != nil {
    		fmt.Println("Error reading file:", err)
    		return
    	}
    	fmt.Println("File content:", string(content))
    }

    Run Instructions:

    filedemo --file=/path/to/sample.txt

    Output:

    File content: Welcome to Go file handling!
    4. Bundling the File within the Binary

    Using the embed package, we can include the file contents directly in the binary.

    Code in main.go:

    package main
    
    import (
    	_ "embed"
    	"fmt"
    )
    
    //go:embed sample.txt
    var fileData []byte
    
    func main() {
    	fmt.Println("File content:", string(fileData))
    }

    Compile and run the binary:

    cd ~/Workspace/filedemo/
    go install
    filedemo

    Output:

    File content: Welcome to Go file handling!
    5. Reading a File in Small Chunks

    For large files, read them in chunks using the bufio package.

    Code in main.go:

    package main
    
    import (
    	"bufio"
    	"flag"
    	"fmt"
    	"io"
    	"os"
    )
    
    func main() {
    	filePath := flag.String("file", "sample.txt", "Path of the file to read")
    	flag.Parse()
    
    	file, err := os.Open(*filePath)
    	if err != nil {
    		fmt.Println("Error opening file:", err)
    		return
    	}
    	defer file.Close()
    
    	reader := bufio.NewReader(file)
    	buffer := make([]byte, 5)
    
    	for {
    		bytesRead, err := reader.Read(buffer)
    		if err == io.EOF {
    			break
    		}
    		if err != nil {
    			fmt.Println("Error reading file:", err)
    			return
    		}
    		fmt.Print(string(buffer[:bytesRead]))
    	}
    	fmt.Println("\nFile read completed.")
    }
    6. Reading a File Line by Line

    To process large files line by line, use a bufio.Scanner.

    Code in main.go:

    package main
    
    import (
    	"bufio"
    	"flag"
    	"fmt"
    	"os"
    )
    
    func main() {
    	filePath := flag.String("file", "sample.txt", "Path of the file to read")
    	flag.Parse()
    
    	file, err := os.Open(*filePath)
    	if err != nil {
    		fmt.Println("Error opening file:", err)
    		return
    	}
    	defer file.Close()
    
    	scanner := bufio.NewScanner(file)
    	for scanner.Scan() {
    		fmt.Println(scanner.Text())
    	}
    
    	if err := scanner.Err(); err != nil {
    		fmt.Println("Error reading file:", err)
    	}
    }

    Run Instructions:

    filedemo --file=/path/to/sample.txt

    Output:

    Welcome to Go file handling!

    Writing Files using Go

    One of the simplest and most common operations is writing a string to a file. The process includes the following steps:

    1. Create a file.
    2. Write the string to the file.
    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    func main() {
    	file, err := os.Create("example.txt")
    	if err != nil {
    		fmt.Println("Error creating file:", err)
    		return
    	}
    	defer file.Close()
    
    	length, err := file.WriteString("Greetings, Universe!")
    	if err != nil {
    		fmt.Println("Error writing to file:", err)
    		return
    	}
    	fmt.Println(length, "characters successfully written.")
    }

    This program creates a file named example.txt. If it already exists, it will be overwritten. The WriteString method writes the string to the file and returns the number of characters written along with any errors. On running the code, you’ll see:

    21 characters successfully written.
    Writing Bytes to a File

    Writing raw bytes is similar to writing strings. The Write method is used for this purpose.

    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    func main() {
    	file, err := os.Create("output_bytes")
    	if err != nil {
    		fmt.Println("Error creating file:", err)
    		return
    	}
    	defer file.Close()
    
    	data := []byte{72, 101, 108, 108, 111, 32, 98, 121, 116, 101, 115}
    	bytesWritten, err := file.Write(data)
    	if err != nil {
    		fmt.Println("Error writing bytes:", err)
    		return
    	}
    	fmt.Println(bytesWritten, "bytes successfully written.")
    }

    This code creates a file output_bytes, writes a slice of bytes corresponding to the string Hello bytes, and outputs the number of bytes written. Expected output:

    11 bytes successfully written.
    Writing Lines to a File

    Often, we need to write multiple lines to a file. The Fprintln function makes this straightforward:

    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    func main() {
    	file, err := os.Create("multi_lines.txt")
    	if err != nil {
    		fmt.Println("Error creating file:", err)
    		return
    	}
    	defer file.Close()
    
    	lines := []string{
    		"Go is fun to learn.",
    		"It is concise and efficient.",
    		"File handling is straightforward.",
    	}
    
    	for _, line := range lines {
    		if _, err := fmt.Fprintln(file, line); err != nil {
    			fmt.Println("Error writing line:", err)
    			return
    		}
    	}
    	fmt.Println("Lines written successfully.")
    }

    This will create a file named multi_lines.txt containing:

    Go is fun to learn.
    It is concise and efficient.
    File handling is straightforward.
    Appending to a File

    To add content to an existing file, open it in append mode:

    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    func main() {
    	file, err := os.OpenFile("multi_lines.txt", os.O_APPEND|os.O_WRONLY, 0644)
    	if err != nil {
    		fmt.Println("Error opening file:", err)
    		return
    	}
    	defer file.Close()
    
    	newLine := "Appending is simple!"
    	if _, err := fmt.Fprintln(file, newLine); err != nil {
    		fmt.Println("Error appending to file:", err)
    		return
    	}
    	fmt.Println("Line appended successfully.")
    }

    This program appends a new line to multi_lines.txt, resulting in:

    Go is fun to learn.
    It is concise and efficient.
    File handling is straightforward.
    Appending is simple!
    Concurrent File Writing

    When multiple goroutines write to a file, coordination is necessary to avoid race conditions. This can be achieved using channels.

    Here’s an example that generates 50 random numbers concurrently and writes them to a file:

    package main
    
    import (
    	"fmt"
    	"math/rand"
    	"os"
    	"sync"
    )
    
    func generateNumbers(data chan int, wg *sync.WaitGroup) {
    	num := rand.Intn(1000)
    	data <- num
    	wg.Done()
    }
    
    func writeToFile(data chan int, done chan bool) {
    	file, err := os.Create("random_numbers.txt")
    	if err != nil {
    		fmt.Println("Error creating file:", err)
    		done <- false
    		return
    	}
    	defer file.Close()
    
    	for num := range data {
    		if _, err := fmt.Fprintln(file, num); err != nil {
    			fmt.Println("Error writing to file:", err)
    			done <- false
    			return
    		}
    	}
    	done <- true
    }
    
    func main() {
    	dataChannel := make(chan int)
    	doneChannel := make(chan bool)
    	var wg sync.WaitGroup
    
    	for i := 0; i < 50; i++ {
    		wg.Add(1)
    		go generateNumbers(dataChannel, &wg)
    	}
    
    	go writeToFile(dataChannel, doneChannel)
    	go func() {
    		wg.Wait()
    		close(dataChannel)
    	}()
    
    	if <-doneChannel {
    		fmt.Println("Random numbers written successfully.")
    	} else {
    		fmt.Println("Failed to write random numbers.")
    	}
    }

    This program creates a file random_numbers.txt containing 50 randomly generated numbers.

  • Reflection in Go

    Reflection

    A language that supports first-class functions allows functions to be:

    • Assigned to variables.
    • Passed as arguments to other functions.
    • Returned from other functions.
    Why Inspect a Variable’s Type at Runtime?

    At first glance, it might seem unnecessary to inspect a variable’s type at runtime since variable types are usually defined at compile time. However, this is not always the case, especially when working with generic code or handling data of unknown types.

    Example:

    package main
    
    import "fmt"
    
    func main() {
    	i := 42
    	fmt.Printf("Value: %d, Type: %T\n", i, i)
    }

    Output:

    Value: 42, Type: int

    Generic Query Generator Example

    package main
    
    type product struct {
    	productId   int
    	productName string
    }
    
    type customer struct {
    	customerName string
    	customerId   int
    	age          int
    }

    We want a function createQuery that works for any struct. For example:

    If given:

    p := product{
        productId:   101,
        productName: "Laptop",
    }

    It should generate:

    c := customer{
        customerName: "Alice",
        customerId:   1,
        age:          30,
    }

    If given:

    c := customer{
        customerName: "Alice",
        customerId:   1,
        age:          30,
    }

    It should generate:

    insert into customer(customerName, customerId, age) values("Alice", 1, 30)
    Using Reflection

    The reflect package in Go provides tools to achieve this. Key components are:

    • reflect.TypeOf: Returns the type of a value.
    • reflect.ValueOf: Returns the value of an interface.
    • reflect.Kind: Indicates the specific kind (e.g., structintstring) of a type.

    Implementation of createQuery

    package main
    
    import (
    	"fmt"
    	"reflect"
    )
    
    func createQuery(q interface{}) {
    	// Ensure the input is a struct
    	if reflect.ValueOf(q).Kind() == reflect.Struct {
    		t := reflect.TypeOf(q)
    		v := reflect.ValueOf(q)
    
    		// Start building the query
    		query := fmt.Sprintf("insert into %s(", t.Name())
    
    		// Add field names
    		for i := 0; i < t.NumField(); i++ {
    			if i > 0 {
    				query += ", "
    			}
    			query += t.Field(i).Name
    		}
    		query += ") values("
    
    		// Add field values
    		for i := 0; i < v.NumField(); i++ {
    			if i > 0 {
    				query += ", "
    			}
    			switch v.Field(i).Kind() {
    			case reflect.Int:
    				query += fmt.Sprintf("%d", v.Field(i).Int())
    			case reflect.String:
    				query += fmt.Sprintf("\"%s\"", v.Field(i).String())
    			default:
    				fmt.Println("Unsupported field type")
    				return
    			}
    		}
    		query += ")"
    		fmt.Println(query)
    	} else {
    		fmt.Println("Unsupported type")
    	}
    }

    Example Usage

    func main() {
    	p := product{
    		productId:   101,
    		productName: "Laptop",
    	}
    	createQuery(p)
    
    	c := customer{
    		customerName: "Alice",
    		customerId:   1,
    		age:          30,
    	}
    	createQuery(c)
    
    	x := 42
    	createQuery(x)
    }

    Output:

    insert into product(productId, productName) values(101, "Laptop")
    insert into customer(customerName, customerId, age) values("Alice", 1, 30)
    Unsupported type

    This implementation demonstrates how reflection allows us to work with types dynamically at runtime, enabling powerful generic programming capabilities.

  • First Class Functions

    First-Class Functions in Go

    A language that supports first-class functions allows functions to be:

    • Assigned to variables.
    • Passed as arguments to other functions.
    • Returned from other functions.
    Anonymous Functions

    Anonymous functions are functions without a name, which can be assigned to variables or invoked immediately.

    Example of Assigning a Function to a Variable:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	greet := func() {
    		fmt.Println("Hello from an anonymous function!")
    	}
    	greet()
    	fmt.Printf("Type of greet: %T", greet)
    }

    Output:

    Hello from an anonymous function!
    func()

    Here, greet holds an anonymous function that is invoked using greet().

    Immediately Invoking an Anonymous Function

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	func() {
    		fmt.Println("Hello, Go developers!")
    	}()
    }

    Output:

    Hello, Go developers!

    Anonymous Function with Arguments

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	func(name string) {
    		fmt.Println("Welcome,", name)
    	}("Developers")
    }

    Output:

    Welcome, Developers
    User-Defined Function Types

    You can define custom types for functions, just like structs.

    type mathOp func(x, y int) int

    The above creates a new type mathOp for functions that take two int arguments and return an int.

    Example:

    package main
    
    import (
    	"fmt"
    )
    
    type mathOp func(x, y int) int
    
    func main() {
    	add := func(x, y int) int {
    		return x + y
    	}
    	var operation mathOp = add
    	fmt.Println("Sum:", operation(3, 7))
    }

    Output:

    Sum: 10
    Higher-Order Functions

    higher-order function:

    • Accepts other functions as arguments.
    • Returns a function as its result.

    Let’s look at some simple examples for the above two scenarios.

    Passing functions as arguments to other functions
    package main
    
    import (
    	"fmt"
    )
    
    func compute(op func(x, y int) int) {
    	fmt.Println("Result:", op(10, 5))
    }
    
    func main() {
    	multiply := func(x, y int) int {
    		return x * y
    	}
    	compute(multiply)
    }

    Output:

    Result: 50
    Returning Functions from Functions
    package main
    
    import (
    	"fmt"
    )
    
    func generateMultiplier(factor int) func(int) int {
    	return func(x int) int {
    		return x * factor
    	}
    }
    
    func main() {
    	double := generateMultiplier(2)
    	fmt.Println("Double of 8:", double(8))
    }

    Output:

    Double of 8: 16
    Closures

    A closure is an anonymous function that captures and uses variables from its surrounding scope.

    Example:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	message := "Hello"
    	func() {
    		fmt.Println(message)
    	}()
    }

    Output:

    Hello
    Independent Closures
    package main
    
    import (
    	"fmt"
    )
    
    func counter() func() int {
    	value := 0
    	return func() int {
    		value++
    		return value
    	}
    }
    
    func main() {
    	c1 := counter()
    	c2 := counter()
    
    	fmt.Println(c1()) // 1
    	fmt.Println(c1()) // 2
    	fmt.Println(c2()) // 1
    }

    Output:

    1
    2
    1
    Practical Examples of First-Class Functions

    Filtering a Slice

    package main
    
    import (
    	"fmt"
    )
    
    type student struct {
    	name   string
    	grade  string
    	country string
    }
    
    func filter(students []student, criteria func(student) bool) []student {
    	var result []student
    	for _, s := range students {
    		if criteria(s) {
    			result = append(result, s)
    		}
    	}
    	return result
    }
    
    func main() {
    	students := []student{
    		{name: "Alice", grade: "A", country: "USA"},
    		{name: "Bob", grade: "B", country: "India"},
    	}
    
    	byGradeB := filter(students, func(s student) bool {
    		return s.grade == "B"
    	})
    	fmt.Println("Students with grade B:", byGradeB)
    }

    Output:

    Students with grade B: [{Bob B India}]

    Map Function

    package main
    
    import (
    	"fmt"
    )
    
    func mapInts(numbers []int, operation func(int) int) []int {
    	var result []int
    	for _, n := range numbers {
    		result = append(result, operation(n))
    	}
    	return result
    }
    
    func main() {
    	numbers := []int{2, 3, 4}
    	squared := mapInts(numbers, func(n int) int {
    		return n * n
    	})
    	fmt.Println("Squared values:", squared)
    }

    Output:

    Squared values: [4 9 16]
  • Defer and Error Handling

    Defer

    The defer statement in Go is used to execute a function call just before the enclosing function returns. 

    Example 1: Basic Usage of Defer

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func logExecutionTime(start time.Time) {
    	fmt.Printf("Execution time: %.2f seconds\n", time.Since(start).Seconds())
    }
    
    func performTask() {
    	start := time.Now()
    	defer logExecutionTime(start)
    	time.Sleep(3 * time.Second)
    	fmt.Println("Task completed")
    }
    
    func main() {
    	performTask()
    }

    Explanation:
    In this program, defer is used to measure the time taken by the performTask function. The start time is passed to the deferred call to logExecutionTime, which gets executed just before the function exits.

    Output:

    Task completed
    Execution time: 3.00 seconds
    Arguments Evaluation

    The arguments of a deferred function are evaluated when the defer statement is executed, not at the time of function execution.

    Example 2: Argument Evaluation

    package main
    
    import "fmt"
    
    func printValue(x int) {
    	fmt.Println("Deferred function received value:", x)
    }
    
    func main() {
    	y := 7
    	defer printValue(y)
    	y = 15
    	fmt.Println("Updated value of y before deferred execution:", y)
    }

    Explanation:
    Here, y initially holds the value 7. When the defer statement is executed, the value of y at that moment is captured (7). Later, even though y is updated to 15, the deferred call uses the value captured at the time the defer statement was executed.

    Output:

    Updated value of y before deferred execution: 15
    Deferred function received value: 7
    Deferred Methods

    Defer works not just with functions but also with methods.

    Example 3: Deferred Method

    package main
    
    import "fmt"
    
    type animal struct {
    	name string
    	kind string
    }
    
    func (a animal) describe() {
    	fmt.Printf("%s is a %s.\n", a.name, a.kind)
    }
    
    func main() {
    	dog := animal{name: "Buddy", kind: "Dog"}
    	defer dog.describe()
    	fmt.Println("Starting program")
    }

    Output:

    Starting program
    Buddy is a Dog.
    Stacking Multiple Defers

    Deferred calls are executed in Last In, First Out (LIFO) order.

    Example 4: Reversing a String Using Deferred Calls

    package main
    
    import "fmt"
    
    func main() {
    	word := "Hello"
    	fmt.Printf("Original Word: %s\n", word)
    	fmt.Printf("Reversed Word: ")
    	for _, char := range word {
    		defer fmt.Printf("%c", char)
    	}
    }

    Explanation:
    Each deferred call to fmt.Printf is pushed onto a stack. When the function exits, these calls are executed in reverse order, printing the string backward.

    Output:

    Original Word: Hello
    Reversed Word: olleH
    Practical Uses of Defer

    Defer is especially useful in scenarios where a function call must be executed regardless of the flow of the program.

    Example 5: Simplified WaitGroup Implementation

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    type rectangle struct {
    	length int
    	width  int
    }
    
    func (r rectangle) calculateArea(wg *sync.WaitGroup) {
    	defer wg.Done()
    	if r.length <= 0 || r.width <= 0 {
    		fmt.Printf("Invalid dimensions for rectangle: %+v\n", r)
    		return
    	}
    	fmt.Printf("Area of rectangle %+v: %d\n", r, r.length*r.width)
    }
    
    func main() {
    	var wg sync.WaitGroup
    	rects := []rectangle{
    		{length: 10, width: 5},
    		{length: -8, width: 4},
    		{length: 6, width: 0},
    	}
    
    	for _, rect := range rects {
    		wg.Add(1)
    		go rect.calculateArea(&wg)
    	}
    
    	wg.Wait()
    	fmt.Println("All goroutines completed")
    }

    Explanation:
    The defer wg.Done() ensures the Done call is executed no matter how the function exits. This simplifies the code, making it easier to read and maintain.

    Output:

    Area of rectangle {length:10 width:5}: 50
    Invalid dimensions for rectangle: {length:-8 width:4}
    Invalid dimensions for rectangle: {length:6 width:0}
    All goroutines completed

    Error Handling

    Errors indicate abnormal conditions occurring in a program. For example, if you try to open a file that does not exist, it leads to an error. In Go, errors are values just like intfloat64, etc. They can be stored in variables, passed as parameters, or returned from functions. Errors in Go are represented using the built-in error type.

    Example:

    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    func main() {
    	file, err := os.Open("nonexistent.txt")
    	if err != nil {
    		fmt.Println("Error occurred:", err)
    		return
    	}
    	fmt.Println(file.Name(), "opened successfully")
    }

    Explanation:

    • The os.Open function attempts to open a file. It returns two values: a file handle and an error.
    • If the file does not exist, err will not be nil. Hence, the program prints the error message and exits.

    Output:

    Error occurred: open nonexistent.txt: no such file or directory
    The error Type

    The error type is an interface defined as follows:

    type error interface {
        Error() string
    }

    Any type implementing the Error() method is considered an error. When you use fmt.Println with an error, it internally calls the Error() method to print the description.

    Extracting More Information from Errors

    1. Converting Errors to Structs: Many errors in Go are returned as struct types that implement the error interface. For example, the os.Open function may return an error of type *os.PathError. The *os.PathError struct is defined as:

    type PathError struct {
        Op   string
        Path string
        Err  error
    }
    
    func (e *PathError) Error() string {
        return e.Op + " " + e.Path + ": " + e.Err.Error()
    }

    To retrieve more information, you can use the errors.As function:

    package main
    
    import (
    	"errors"
    	"fmt"
    	"os"
    )
    
    func main() {
    	_, err := os.Open("invalidfile.txt")
    	if err != nil {
    		var pathErr *os.PathError
    		if errors.As(err, &pathErr) {
    			fmt.Println("Error occurred while accessing:", pathErr.Path)
    			return
    		}
    		fmt.Println("General error:", err)
    	}
    }

    Output:

    Error occurred while accessing: invalidfile.txt

    2. Using Methods on Structs: Some error structs have additional methods. For example, the net.DNSError struct provides methods to check if the error is due to a timeout or is temporary:

    Example:

    package main
    
    import (
    	"errors"
    	"fmt"
    	"net"
    )
    
    func main() {
    	_, err := net.LookupHost("invalidhost.example")
    	if err != nil {
    		var dnsErr *net.DNSError
    		if errors.As(err, &dnsErr) {
    			if dnsErr.Timeout() {
    				fmt.Println("Operation timed out")
    			} else if dnsErr.Temporary() {
    				fmt.Println("Temporary DNS error")
    			} else {
    				fmt.Println("Generic DNS error:", dnsErr)
    			}
    			return
    		}
    		fmt.Println("Other error:", err)
    	}
    }

    Output:

    Generic DNS error: lookup invalidhost.example: no such host

    3. Direct Comparison: Some errors are defined as variables in the standard library, allowing direct comparison. For example, the filepath.Glob function returns the filepath.ErrBadPattern error if the pattern is invalid:

    package main
    
    import (
    	"errors"
    	"fmt"
    	"path/filepath"
    )
    
    func main() {
    	_, err := filepath.Glob("[")
    	if err != nil {
    		if errors.Is(err, filepath.ErrBadPattern) {
    			fmt.Println("Invalid pattern:", err)
    			return
    		}
    		fmt.Println("Other error:", err)
    	}
    }

    Output:

    No tasks are ready yet
    Ignoring Errors (Not Recommended)

    Ignoring errors can lead to unexpected behavior. 

    Example:

    package main
    
    import (
    	"fmt"
    	"path/filepath"
    )
    
    func main() {
    	files, _ := filepath.Glob("[")
    	fmt.Println("Matched files:", files)
    }

    Output:

    Matched files: []

    Custom Errors

    Lets learn how to create custom errors for functions and packages, using techniques inspired by the standard library to provide detailed error information.

    Creating Custom Errors with the New Function

    The simplest way to create a custom error is by using the New function from the errors package. Before we use it, let’s examine its implementation in the errors package:

    package errors
    
    // New returns an error that formats as the given text.
    // Each call to New returns a distinct error value even if the text is identical.
    func New(text string) error {
        return &errorString{text}
    }
    
    // errorString is a trivial implementation of error.
    type errorString struct {
        s string
    }
    
    func (e *errorString) Error() string {
        return e.s
    }

    This implementation is straightforward. The errorString struct contains a single field s for the error message. The Error() method implements the error interface. The New function creates an errorString value, takes its address, and returns it as an error.

    Example: Validating a Triangle’s Sides

    Let’s write a program to validate whether three given sides can form a triangle. If any side is negative, the function will return an error.

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    func validateTriangle(a, b, c float64) error {
    	if a < 0 || b < 0 || c < 0 {
    		return errors.New("Triangle validation failed: one or more sides are negative")
    	}
    	return nil
    }
    
    func main() {
    	a, b, c := 3.0, -4.0, 5.0
    	err := validateTriangle(a, b, c)
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	fmt.Println("The triangle sides are valid.")
    }

    Output:

    Triangle validation failed: one or more sides are negative
    Adding More Details Using Errorf

    To provide more information, such as which side caused the error, we can use the Errorf function from the fmt package. It formats the error string with placeholders.

    Example: Using Errorf to Identify Invalid Side

    package main
    
    import (
    	"fmt"
    )
    
    func validateTriangle(a, b, c float64) error {
    	if a < 0 {
    		return fmt.Errorf("Triangle validation failed: side a (%0.2f) is negative", a)
    	}
    	if b < 0 {
    		return fmt.Errorf("Triangle validation failed: side b (%0.2f) is negative", b)
    	}
    	if c < 0 {
    		return fmt.Errorf("Triangle validation failed: side c (%0.2f) is negative", c)
    	}
    	return nil
    }
    
    func main() {
    	a, b, c := 3.0, -4.0, 5.0
    	err := validateTriangle(a, b, c)
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	fmt.Println("The triangle sides are valid.")
    }
    Triangle validation failed: side b (-4.00) is negative
    Using Struct Types for Detailed Errors

    Struct types can add flexibility, allowing access to fields with specific error-related information. This approach eliminates the need to parse error strings.

    Example: Custom Error with Struct Fields

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    type triangleError struct {
    	sideName string
    	sideValue float64
    	err       string
    }
    
    func (e *triangleError) Error() string {
    	return fmt.Sprintf("Triangle validation failed: side %s (%0.2f) is invalid - %s", e.sideName, e.sideValue, e.err)
    }
    
    func validateTriangle(a, b, c float64) error {
    	if a < 0 {
    		return &triangleError{"a", a, "side is negative"}
    	}
    	if b < 0 {
    		return &triangleError{"b", b, "side is negative"}
    	}
    	if c < 0 {
    		return &triangleError{"c", c, "side is negative"}
    	}
    	return nil
    }
    
    func main() {
    	a, b, c := 3.0, -4.0, 5.0
    	err := validateTriangle(a, b, c)
    	if err != nil {
    		var tErr *triangleError
    		if errors.As(err, &tErr) {
    			fmt.Printf("Error: side %s is invalid, value: %0.2f\n", tErr.sideName, tErr.sideValue)
    			return
    		}
    		fmt.Println(err)
    		return
    	}
    	fmt.Println("The triangle sides are valid.")
    }
    Error: side b is invalid, value: -4.00
    Using Methods for Additional Insights

    Methods on the custom error type can provide specific insights.

    Example: Identifying Invalid Sides

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    type triangleError struct {
    	err       string
    	a, b, c   float64
    }
    
    func (e *triangleError) Error() string {
    	return e.err
    }
    
    func (e *triangleError) isSideANegative() bool {
    	return e.a < 0
    }
    
    func (e *triangleError) isSideBNegative() bool {
    	return e.b < 0
    }
    
    func (e *triangleError) isSideCNegative() bool {
    	return e.c < 0
    }
    
    func validateTriangle(a, b, c float64) error {
    	err := ""
    	if a < 0 {
    		err += "side a is negative"
    	}
    	if b < 0 {
    		if err != "" {
    			err += ", "
    		}
    		err += "side b is negative"
    	}
    	if c < 0 {
    		if err != "" {
    			err += ", "
    		}
    		err += "side c is negative"
    	}
    	if err != "" {
    		return &triangleError{err, a, b, c}
    	}
    	return nil
    }
    
    func main() {
    	a, b, c := -3.0, -4.0, 5.0
    	err := validateTriangle(a, b, c)
    	if err != nil {
    		var tErr *triangleError
    		if errors.As(err, &tErr) {
    			if tErr.isSideANegative() {
    				fmt.Printf("Error: side a (%0.2f) is negative\n", tErr.a)
    			}
    			if tErr.isSideBNegative() {
    				fmt.Printf("Error: side b (%0.2f) is negative\n", tErr.b)
    			}
    			if tErr.isSideCNegative() {
    				fmt.Printf("Error: side c (%0.2f) is negative\n", tErr.c)
    			}
    			return
    		}
    		fmt.Println(err)
    		return
    	}
    	fmt.Println("The triangle sides are valid.")
    }
    Error: side a (-3.00) is negative
    Error: side b (-4.00) is negative

    Error Wrapping

    Understanding Error Wrapping

    Error wrapping involves encapsulating one error into another. Imagine we have a web service that accesses a database to fetch a record. If the database call results in an error, we can choose to wrap this error or return a custom error message. Let’s look at an example to clarify:

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    var recordNotFound = errors.New("record not found")
    
    func fetchRecord() error {
    	return recordNotFound
    }
    
    func serviceHandler() error {
    	if err := fetchRecord(); err != nil {
    		return fmt.Errorf("Service Error: %s during database query", err)
    	}
    	return nil
    }
    
    func main() {
    	if err := serviceHandler(); err != nil {
    		fmt.Printf("Service failed with: %s\n", err)
    		return
    	}
    	fmt.Println("Service executed successfully")
    }

    In this example, we send a string representation of the error encountered in fetchRecord back from serviceHandler

    Error Wrapping with errors.Is

    The Is function in the errors package checks whether any error in the chain matches a target error. In the previous example, the error from fetchRecord is returned as a formatted string from serviceHandler, which breaks error wrapping. Let’s modify the main function to demonstrate:

    func main() {
    	if err := serviceHandler(); err != nil {
    		if errors.Is(err, recordNotFound) {
    			fmt.Printf("The record cannot be retrieved. Database error: %s\n", err)
    			return
    		}
    		fmt.Println("An unknown error occurred during record retrieval")
    		return
    	}
    	fmt.Println("Service executed successfully")
    }

    Here, the Is function in line 4 checks whether any error in the chain matches the recordNotFound error. However, this won’t work because the error isn’t wrapped correctly. To fix this, we can use the %w format specifier to wrap the error properly.

    Modify the error return in serviceHandler to:

    return fmt.Errorf("Service Error: %w during database query", err)

    The complete corrected program:

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    var recordNotFound = errors.New("record not found")
    
    func fetchRecord() error {
    	return recordNotFound
    }
    
    func serviceHandler() error {
    	if err := fetchRecord(); err != nil {
    		return fmt.Errorf("Service Error: %w during database query", err)
    	}
    	return nil
    }
    
    func main() {
    	if err := serviceHandler(); err != nil {
    		if errors.Is(err, recordNotFound) {
    			fmt.Printf("The record cannot be retrieved. Database error: %s\n", err)
    			return
    		}
    		fmt.Println("An unknown error occurred during record retrieval")
    		return
    	}
    	fmt.Println("Service executed successfully")
    }

    Output:

    The record cannot be retrieved. Database error: Service Error: record not found during database query
    Error Wrapping with errors.As

    The As function in the errors package attempts to convert an error to a target type. If successful, it sets the target to the first matching error in the chain and returns trueExample:

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    type ServiceError struct {
    	message string
    }
    
    func (e ServiceError) Error() string {
    	return e.message
    }
    
    func fetchRecord() error {
    	return ServiceError{
    		message: "record not found",
    	}
    }
    
    func serviceHandler() error {
    	if err := fetchRecord(); err != nil {
    		return fmt.Errorf("Service Error: %w during database query", err)
    	}
    	return nil
    }
    
    func main() {
    	if err := serviceHandler(); err != nil {
    		var svcErr ServiceError
    		if errors.As(err, &svcErr) {
    			fmt.Printf("Record retrieval failed. Error details: %s\n", svcErr)
    			return
    		}
    		fmt.Println("An unexpected error occurred during record retrieval")
    		return
    	}
    	fmt.Println("Service executed successfully")
    }

    In this example, the fetchRecord function returns a custom error of type ServiceError. The errors.As function in line 27 attempts to cast the error returned from serviceHandler into the ServiceError type. If successful, it prints the error message.

    Output:

    Record retrieval failed. Error details: record not found

    Panic and Recover

    Error Wrapping

    Error wrapping involves encapsulating one error into another. Imagine we have a web service that accesses a database to fetch a record. If the database call results in an error, we can choose to wrap this error or return a custom error message.

    Example:

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    var recordNotFound = errors.New("record not found")
    
    func fetchRecord() error {
    	return recordNotFound
    }
    
    func serviceHandler() error {
    	if err := fetchRecord(); err != nil {
    		return fmt.Errorf("Service Error: %s during database query", err)
    	}
    	return nil
    }
    
    func main() {
    	if err := serviceHandler(); err != nil {
    		fmt.Printf("Service failed with: %s\n", err)
    		return
    	}
    	fmt.Println("Service executed successfully")
    }

    In this example, we send a string representation of the error encountered in fetchRecord back from serviceHandler.

    Error Wrapping with errors.Is

    The Is function in the errors package checks whether any error in the chain matches a target error. In the previous example, the error from fetchRecord is returned as a formatted string from serviceHandler, which breaks error wrapping. Let’s modify the main function to demonstrate:

    func main() {
    	if err := serviceHandler(); err != nil {
    		if errors.Is(err, recordNotFound) {
    			fmt.Printf("The record cannot be retrieved. Database error: %s\n", err)
    			return
    		}
    		fmt.Println("An unknown error occurred during record retrieval")
    		return
    	}
    	fmt.Println("Service executed successfully")
    }

    Here, the Is function in line 4 checks whether any error in the chain matches the recordNotFound error. However, this won’t work because the error isn’t wrapped correctly. To fix this, we can use the %w format specifier to wrap the error properly.

    Modify the error return in serviceHandler to:

    return fmt.Errorf("Service Error: %w during database query", err)

    The complete corrected program:

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    var recordNotFound = errors.New("record not found")
    
    func fetchRecord() error {
    	return recordNotFound
    }
    
    func serviceHandler() error {
    	if err := fetchRecord(); err != nil {
    		return fmt.Errorf("Service Error: %w during database query", err)
    	}
    	return nil
    }
    
    func main() {
    	if err := serviceHandler(); err != nil {
    		if errors.Is(err, recordNotFound) {
    			fmt.Printf("The record cannot be retrieved. Database error: %s\n", err)
    			return
    		}
    		fmt.Println("An unknown error occurred during record retrieval")
    		return
    	}
    	fmt.Println("Service executed successfully")
    }

    Output:

    The record cannot be retrieved. Database error: Service Error: record not found during database query
    Error Wrapping with errors.As

    The As function in the errors package attempts to convert an error to a target type. If successful, it sets the target to the first matching error in the chain and returns true.

    Example:

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    type ServiceError struct {
    	message string
    }
    
    func (e ServiceError) Error() string {
    	return e.message
    }
    
    func fetchRecord() error {
    	return ServiceError{
    		message: "record not found",
    	}
    }
    
    func serviceHandler() error {
    	if err := fetchRecord(); err != nil {
    		return fmt.Errorf("Service Error: %w during database query", err)
    	}
    	return nil
    }
    
    func main() {
    	if err := serviceHandler(); err != nil {
    		var svcErr ServiceError
    		if errors.As(err, &svcErr) {
    			fmt.Printf("Record retrieval failed. Error details: %s\n", svcErr)
    			return
    		}
    		fmt.Println("An unexpected error occurred during record retrieval")
    		return
    	}
    	fmt.Println("Service executed successfully")
    }

    In this example, the fetchRecord function returns a custom error of type ServiceError. The errors.As function in line 27 attempts to cast the error returned from serviceHandler into the ServiceError type. If successful, it prints the error message.

    Output:

    Record retrieval failed. Error details: record not found

    Output:

    Hello
    World
    Go
    Channel Properties

    1.Length of a Channel: Use len() to find the number of elements currently in the channel.

    Example:

    // Go program to find channel length
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
    
        fmt.Println("Channel length:", len(ch))
    }

    Output:

    Channel length: 2

    2. Capacity of a Channel: Use cap() to find the total capacity of the channel.

    Example:

    // Go program to find channel capacity
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan float64, 4)
    
        ch <- 1.1
        ch <- 2.2
    
        fmt.Println("Channel capacity:", cap(ch))
    }

    Output:

    Channel capacity: 4

    Unidirectional Channel in Golang

    In Golang, channels act as a communication mechanism between concurrently running goroutines, allowing them to transmit and receive data. By default, channels in Go are bidirectional, meaning they support both sending and receiving operations. However, it’s possible to create unidirectional channels, which can either exclusively send or receive data. These unidirectional channels can be constructed using the make() function as demonstrated below:

    // For receiving data only
    c1 := make(<-chan bool)
    
    // For sending data only
    c2 := make(chan<- bool)

    Example:

    // Go program to demonstrate the concept
    // of unidirectional channels
    package main
    
    import "fmt"
    
    func main() {
        // Channel restricted to receiving data
        recvOnly := make(<-chan int)
    
        // Channel restricted to sending data
        sendOnly := make(chan<- int)
    
        // Display the types of the channels
        fmt.Printf("%T", recvOnly)
        fmt.Printf("\n%T", sendOnly)
    }

    Output:

    <-chan int
    chan<- int
    Converting Bidirectional Channels into Unidirectional Channels

    In Go, you can convert a bidirectional channel into a unidirectional channel, meaning you can restrict it to either sending or receiving data. However, the reverse conversion (from unidirectional back to bidirectional) is not possible. This concept is illustrated in the following example:

    Example:

    // Go program to illustrate conversion
    // of a bidirectional channel into a
    // unidirectional channel
    package main
    
    import "fmt"
    
    // Function to send data through a send-only channel
    func sendData(channel chan<- string) {
        channel <- "Hello from Golang"
    }
    
    func main() {
        // Creating a bidirectional channel
        bidiChannel := make(chan string)
    
        // Passing the bidirectional channel to a function,
        // which restricts it to a send-only channel
        go sendData(bidiChannel)
    
        // Receiving data from the channel
        fmt.Println(<-bidiChannel)
    }

    Output:

    Hello from Golang
  • Object Oriented Programming

    Is Go Object Oriented?

    Go is not considered a pure object-oriented programming language. However, as outlined in Go’s FAQs, it incorporates features that allow object-oriented programming to some extent.

    Does Go support object-oriented programming?

    Yes and no. While Go includes types, methods, and enables an object-oriented style of programming, it does not feature a traditional type hierarchy. Instead, Go utilizes the concept of “interfaces,” offering a more flexible and generalized approach. Additionally, Go supports embedding types into other types, achieving functionality similar—but not identical—to subclassing. Methods in Go are versatile and can be defined for any kind of data, including basic types like integers, not just structs (classes).

    Upcoming sections will explore how Go implements object-oriented concepts, which differ in approach compared to languages like Java or C++.

    Structs Instead of Classes

    In Go, structs act as a substitute for classes. Structs can have associated methods, allowing data and behaviors to be bundled together in a manner similar to a class.

    Let’s dive into an example to understand this concept better.

    We’ll create a custom package to demonstrate how structs can effectively replace classes.

    Steps to Create the Example

    1. Set Up the Directory
      • Create a subfolder in ~/Projects/ named oop.
      • Inside the oop directory, initialize a Go module by running:
    go mod init oop

    2. Create a Subfolder and Add Files

    • Add a subfolder named product inside the oop folder.
    • Inside product, create a file named product.go.

    Folder Structure:

    ├── Projects
    │   └── oop
    │       ├── product
    │       │   └── product.go
    │       └── go.mod

    3. Code for product.go
    Replace the contents of product.go with:

    package product
    
    import "fmt"
    
    type Product struct {
        Name      string
        Price     float64
        Stock     int
    }
    
    func (p Product) StockValue() {
        fmt.Printf("The total value of %s stock is %.2f\n", p.Name, p.Price*float64(p.Stock))
    }
    • The Product struct bundles product data: NamePrice, and Stock.
    • The StockValue method calculates the total value of the stock for the product.

    4. Create main.go
    Add a new file, main.go, in the oop folder.

    Folder Structure:

    ├── Projects
    │   └── oop
    │       ├── product
    │       │   └── product.go
    │       ├── go.mod
    │       └── main.go

    5. Code for main.go
    Replace the contents of main.go with:

    package main
    
    import "oop/product"
    
    func main() {
        p := product.Product{
            Name:  "Laptop",
            Price: 50000.0,
            Stock: 10,
        }
        p.StockValue()
    }

    6. Run the Program
    Use the following commands:

    go install
    oop

    Output:

    The total value of Laptop stock is 500000.00
    The New() Function as a Constructor

    The program above works fine, but what happens when a Product struct is initialized with zero values? Modify main.go as follows:

    package main
    
    import "oop/product"
    
    func main() {
        var p product.Product
        p.StockValue()
    }

    Output:

    The total value of  stock is 0.00

    Select Statement

    In Go, the select statement allows you to wait for multiple channel operations to complete, such as sending or receiving values. Similar to a switch statement, select lets you proceed with the first available case, making it ideal for managing concurrent operations and handling asynchronous tasks effectively.

    Example

    Imagine you have two tasks that finish at different times. You can use select to receive data from whichever task completes first.

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func task1(ch chan string) {
        time.Sleep(2 * time.Second)
        ch <- "Task 1 finished"
    }
    
    func task2(ch chan string) {
        time.Sleep(4 * time.Second)
        ch <- "Task 2 finished"
    }
    
    func main() {
        ch1 := make(chan string)
        ch2 := make(chan string)
    
        go task1(ch1)
        go task2(ch2)
    
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }

    Output:

    Task 1 finished

    In this example, “Task 1 finished” will be printed after 2 seconds, as task1 finishes before task2. If task2 had completed first, the output would have been “Task 2 finished.”

    Syntax

    The select statement in Go listens to multiple channel operations and proceeds with the first ready case.

    select {
        case value := <-channel1:
            // Executes if channel1 is ready to send/receive
        case channel2 <- value:
            // Executes if channel2 is ready to send/receive
        default:
            // Executes if no other case is ready
    }

    Key Points:

    • The select statement waits until at least one channel operation is ready.
    • If multiple channels are ready, one is selected at random.
    • The default case is executed if no other case is ready, preventing the program from blocking.
    Select Statement Variations

     Basic Blocking Behavior: In this variation, we modify the example to remove the select statement and see the blocking behavior when no channels are ready.

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        ch := make(chan string)
    
        select {
        case msg := <-ch:
            fmt.Println(msg)
        default:
            fmt.Println("No channels are ready")
        }
    }

    Output:

    No channels are ready

    Handling Multiple Cases: If multiple tasks are ready at the same time, select chooses one case randomly. This can occur if tasks have nearly the same completion times.

    Example:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func portal1(channel1 chan string) {
        time.Sleep(3 * time.Second)
        channel1 <- "Welcome from portal 1"
    }
    
    func portal2(channel2 chan string) {
        time.Sleep(9 * time.Second)
        channel2 <- "Welcome from portal 2"
    }
    
    func main() {
        R1 := make(chan string)
        R2 := make(chan string)
    
        go portal1(R1)
        go portal2(R2)
    
        select {
        case op1 := <-R1:
            fmt.Println(op1)
        case op2 := <-R2:
            fmt.Println(op2)
        }
    }

    Output:

    Welcome from portal 1

    Using Select with Default Case to Avoid Blocking: The default case can be used to avoid blocking when no cases are ready. Here’s an example of modifying the structure to include the default case.

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        ch1 := make(chan string)
        ch2 := make(chan string)
    
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        default:
            fmt.Println("No tasks are ready yet")
        }
    }

    Output:

    No tasks are ready yet

    Infinite Blocking without Cases: select statement with no cases will block indefinitely. This is commonly used when you need an infinite wait.

    package main
    
    func main() {
        select {} // This blocks indefinitely because no cases are present
    }

    Output:

    Welcome to Go Programming

    Multiple Goroutines

    A Goroutine is essentially a function or method that operates independently and concurrently with other Goroutines in a program. In simpler terms, any concurrently executing task in the Go programming language is referred to as a Goroutine. The Go language provides the capability to create multiple Goroutines within a single program. A Goroutine can be initiated by prefixing the go keyword to a function or method call, as demonstrated in the syntax below:

    func functionName() {
        // Statements
    }
    
    // Initiating a Goroutine by prefixing the function call with the go keyword
    go functionName()

    Example: Managing Multiple Goroutines in Go

    // Go program demonstrating Multiple Goroutines
    package main
    
    import (
        "fmt"
        "time"
    )
    
    // For the first Goroutine
    func displayNames() {
        names := [4]string{"Alice", "Bob", "Charlie", "Diana"}
    
        for i := 0; i < len(names); i++ {
            time.Sleep(200 * time.Millisecond)
            fmt.Printf("%s\n", names[i])
        }
    }
    
    // For the second Goroutine
    func displayAges() {
        ages := [4]int{25, 30, 35, 40}
    
        for j := 0; j < len(ages); j++ {
            time.Sleep(400 * time.Millisecond)
            fmt.Printf("%d\n", ages[j])
        }
    }
    
    // Main function
    func main() {
        fmt.Println(">>> Main Goroutine Starts <<<")
    
        // Initiating the first Goroutine
        go displayNames()
    
        // Initiating the second Goroutine
        go displayAges()
    
        // Allowing time for Goroutines to complete
        time.Sleep(2500 * time.Millisecond)
        fmt.Println("\n>>> Main Goroutine Ends <<<")
    }

    Output:

    >>> Main Goroutine Starts <<<
    Alice
    25
    Bob
    Charlie
    30
    Diana
    35
    40
    
    >>> Main Goroutine Ends <<<

    The first portion of the image represents the numbers Goroutine, the second portion represents the alphabets Goroutine, the third portion  represents the main Goroutine and the final portion in black merges all the above three and shows us how the program works. The strings like 0 ms, 250 ms at the top of each box represent the time in milliseconds and the output is represented in the bottom of each box as 1, 2, 3 and so on. The blue box tells us that 1 is printed after 250 ms, 2 is printed after 500 ms and so on. The bottom of the last black box has values 1 a 2 3 b 4 c 5 d e main terminated which is the output of the program as well. The image is self-explanatory and you will be able to understand how the program works.

    Channels in Go Language

    channel in Go is a medium that enables communication between goroutines without using explicit locks. Channels facilitate the exchange of data between goroutines in a synchronized manner, and by default, they are bidirectional. This means the same channel can be used for both sending and receiving data. Below is a detailed explanation and examples of how channels work in Go.

    Creating a Channel; In Go, you use the chan keyword to create a channel. A channel can only transport data of a specific type, and you cannot use the same channel to transfer different data types.

    Syntax:

    var channelName chan Type

    Example:

    // Go program to demonstrate channel creation
    package main
    
    import "fmt"
    
    func main() {
        // Creating a channel using var
        var myChannel chan string
        fmt.Println("Channel value:", myChannel)
        fmt.Printf("Channel type: %T\n", myChannel)
    
        // Creating a channel using make()
        anotherChannel := make(chan string)
        fmt.Println("Another channel value:", anotherChannel)
        fmt.Printf("Another channel type: %T\n", anotherChannel)
    }

    Output:

    Channel value: <nil>
    Channel type: chan string
    Another channel value: 0xc00007c060
    Another channel type: chan string
    Sending and Receiving Data in a Channel

    Channels in Go operate through two primary actions: sending and receiving, collectively referred to as communication. These operations use the <- operator to indicate the direction of the data flow.

    1. Sending Data: The send operation transfers data from one goroutine to another via a channel. For basic data types like integers, floats, and strings, sending is straightforward and safe. However, when working with pointers or references (like slices or maps), ensure that only one goroutine accesses them at a time.

    myChannel <- value

    2. Receiving Data: The receive operation fetches the data from a channel that was sent by another goroutin

    var channelName chan Type

    Example:

    // Go program to demonstrate send and receive operations
    package main
    
    import "fmt"
    
    func calculateSquare(ch chan int) {
        num := <-ch
        fmt.Println("Square of the number is:", num*num)
    }
    
    func main() {
        fmt.Println("Main function starts")
    
        // Creating a channel
        ch := make(chan int)
    
        go calculateSquare(ch)
    
        ch <- 12
    
        fmt.Println("Main function ends")
    }

    Output:

    Main function starts
    Square of the number is: 144
    Main function ends

    Closing a Channel: You can close a channel using the close() function, which indicates that no more data will be sent to that channel.

    Syntax:

    close(channelName)

    When iterating over a channel using a for range loop, the receiver can determine if the channel is open or closed.

    Syntax:

    value, ok := <-channelName

    If ok is true, the channel is open, and you can read data. If ok is false, the channel is closed.

    Example:

    // Go program to close a channel using for-range loop
    package main
    
    import "fmt"
    
    func sendData(ch chan int) {
        for i := 1; i <= 5; i++ {
            ch <- i
        }
        close(ch)
    }
    
    func main() {
        ch := make(chan int)
    
        go sendData(ch)
    
        for value := range ch {
            fmt.Println("Received value:", value)
        }
        fmt.Println("Channel closed")
    }

    Output:

    Received value: 1
    Received value: 2
    Received value: 3
    Received value: 4
    Received value: 5
    Channel closed
    Key Points to Remember
    1. Blocking Behavior:
      • Sending data blocks until another goroutine is ready to receive it.
      • Receiving data blocks until another goroutine sends it.
    2. Nil Channels:
      • A zero-value channel is nil, and operations on it will block indefinitely.
    3. Iterating Over Channels:
      • for range loop can iterate over the values in a channel until it’s closed.

    Example:

    // Go program to iterate over channel using for-range
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan string)
    
        go func() {
            ch <- "Hello"
            ch <- "World"
            ch <- "Go"
            close(ch)
        }()
    
        for msg := range ch {
            fmt.Println(msg)
        }
    }

    Output:

    Hello
    World
    Go
    Channel Properties

    1.Length of a Channel: Use len() to find the number of elements currently in the channel.

    Example:

    // Go program to find channel length
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
    
        fmt.Println("Channel length:", len(ch))
    }

    Output:

    Channel length: 2

    2. Capacity of a Channel: Use cap() to find the total capacity of the channel.

    Example:

    // Go program to find channel capacity
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan float64, 4)
    
        ch <- 1.1
        ch <- 2.2
    
        fmt.Println("Channel capacity:", cap(ch))
    }

    Output:

    Channel capacity: 4

    Unidirectional Channel in Golang

    In Golang, channels act as a communication mechanism between concurrently running goroutines, allowing them to transmit and receive data. By default, channels in Go are bidirectional, meaning they support both sending and receiving operations. However, it’s possible to create unidirectional channels, which can either exclusively send or receive data. These unidirectional channels can be constructed using the make() function as demonstrated below:

    // For receiving data only
    c1 := make(<-chan bool)
    
    // For sending data only
    c2 := make(chan<- bool)

    Example:

    // Go program to demonstrate the concept
    // of unidirectional channels
    package main
    
    import "fmt"
    
    func main() {
        // Channel restricted to receiving data
        recvOnly := make(<-chan int)
    
        // Channel restricted to sending data
        sendOnly := make(chan<- int)
    
        // Display the types of the channels
        fmt.Printf("%T", recvOnly)
        fmt.Printf("\n%T", sendOnly)
    }

    Output:

    <-chan int
    chan<- int
    Converting Bidirectional Channels into Unidirectional Channels

    In Go, you can convert a bidirectional channel into a unidirectional channel, meaning you can restrict it to either sending or receiving data. However, the reverse conversion (from unidirectional back to bidirectional) is not possible. This concept is illustrated in the following example:

    Example:

    // Go program to illustrate conversion
    // of a bidirectional channel into a
    // unidirectional channel
    package main
    
    import "fmt"
    
    // Function to send data through a send-only channel
    func sendData(channel chan<- string) {
        channel <- "Hello from Golang"
    }
    
    func main() {
        // Creating a bidirectional channel
        bidiChannel := make(chan string)
    
        // Passing the bidirectional channel to a function,
        // which restricts it to a send-only channel
        go sendData(bidiChannel)
    
        // Receiving data from the channel
        fmt.Println(<-bidiChannel)
    }

    Output:

    Hello from Golang
  • Concurrency in Golang

    Goroutines

    Goroutines allow functions to run concurrently and consume significantly less memory compared to traditional threads. Every Go program begins execution with a primary Goroutine, commonly referred to as the main Goroutine. If the main Goroutine exits, all other active Goroutines are terminated immediately.

    Syntax:

    func functionName() {
        // statements
    }
    
    // To execute as a Goroutine
    go functionName()

    Example:

    package main
    
    import "fmt"
    
    func showMessage(msg string) {
        for i := 0; i < 3; i++ {
            fmt.Println(msg)
        }
    }
    
    func main() {
        go showMessage("Hello, Concurrent World!") // Executes concurrently
        showMessage("Hello from Main!")
    }
    Creating a Goroutine

    To initiate a Goroutine, simply use the go keyword as a prefix when calling a function or method.

    Syntax:

    func functionName() {
        // statements
    }
    
    // Using `go` keyword to execute the function as a Goroutine
    go functionName()

    Example:

    package main
    import "fmt"
    
    func printMessage(message string) {
        for i := 0; i < 3; i++ {
            fmt.Println(message)
        }
    }
    
    func main() {
        go printMessage("Welcome to Goroutines!") // Executes concurrently
        printMessage("Running in Main!")
    }

    Output:

    Running in Main!
    Running in Main!
    Running in Main!
    Running Goroutines with Delay

    Incorporating time.Sleep() allows sufficient time for both the main and additional Goroutines to execute completely.

    Example:

    package main
    import (
        "fmt"
        "time"
    )
    
    func printMessage(msg string) {
        for i := 0; i < 3; i++ {
            time.Sleep(300 * time.Millisecond)
            fmt.Println(msg)
        }
    }
    
    func main() {
        go printMessage("Executing in Goroutine!")
        printMessage("Executing in Main!")
    }

    Output:

    Executing in Main!
    Executing in Goroutine!
    Executing in Goroutine!
    Executing in Main!
    Executing in Goroutine!
    Executing in Main!
    Anonymous Goroutines

    You can also run anonymous functions as Goroutines by appending the go keyword before the function.

    Syntax

    go func(parameters) {
        // function body
    }(arguments)

    Example:

    package main
    import (
        "fmt"
        "time"
    )
    
    func main() {
        go func(msg string) {
            for i := 0; i < 3; i++ {
                fmt.Println(msg)
                time.Sleep(400 * time.Millisecond)
            }
        }("Anonymous Goroutine Execution!")
    
        time.Sleep(1.5 * time.Second) // Wait for Goroutine to complete
        fmt.Println("Main Goroutine Ends.")
    }

    Output:

    Anonymous Goroutine Execution!
    Anonymous Goroutine Execution!
    Anonymous Goroutine Execution!
    Main Goroutine Ends.

    Select Statement

    In Go, the select statement allows you to wait for multiple channel operations to complete, such as sending or receiving values. Similar to a switch statement, select lets you proceed with the first available case, making it ideal for managing concurrent operations and handling asynchronous tasks effectively.

    Example

    Imagine you have two tasks that finish at different times. You can use select to receive data from whichever task completes first.

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func task1(ch chan string) {
        time.Sleep(2 * time.Second)
        ch <- "Task 1 finished"
    }
    
    func task2(ch chan string) {
        time.Sleep(4 * time.Second)
        ch <- "Task 2 finished"
    }
    
    func main() {
        ch1 := make(chan string)
        ch2 := make(chan string)
    
        go task1(ch1)
        go task2(ch2)
    
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }

    Output:

    Task 1 finished

    In this example, “Task 1 finished” will be printed after 2 seconds, as task1 finishes before task2. If task2 had completed first, the output would have been “Task 2 finished.”

    Syntax

    The select statement in Go listens to multiple channel operations and proceeds with the first ready case.

    select {
        case value := <-channel1:
            // Executes if channel1 is ready to send/receive
        case channel2 <- value:
            // Executes if channel2 is ready to send/receive
        default:
            // Executes if no other case is ready
    }

    Key Points:

    • The select statement waits until at least one channel operation is ready.
    • If multiple channels are ready, one is selected at random.
    • The default case is executed if no other case is ready, preventing the program from blocking.
    Select Statement Variations

     Basic Blocking Behavior: In this variation, we modify the example to remove the select statement and see the blocking behavior when no channels are ready.

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        ch := make(chan string)
    
        select {
        case msg := <-ch:
            fmt.Println(msg)
        default:
            fmt.Println("No channels are ready")
        }
    }

    Output:

    No channels are ready

    Handling Multiple Cases: If multiple tasks are ready at the same time, select chooses one case randomly. This can occur if tasks have nearly the same completion times.

    Example:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func portal1(channel1 chan string) {
        time.Sleep(3 * time.Second)
        channel1 <- "Welcome from portal 1"
    }
    
    func portal2(channel2 chan string) {
        time.Sleep(9 * time.Second)
        channel2 <- "Welcome from portal 2"
    }
    
    func main() {
        R1 := make(chan string)
        R2 := make(chan string)
    
        go portal1(R1)
        go portal2(R2)
    
        select {
        case op1 := <-R1:
            fmt.Println(op1)
        case op2 := <-R2:
            fmt.Println(op2)
        }
    }

    Output:

    Welcome from portal 1

    Using Select with Default Case to Avoid Blocking: The default case can be used to avoid blocking when no cases are ready. Here’s an example of modifying the structure to include the default case.

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        ch1 := make(chan string)
        ch2 := make(chan string)
    
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        default:
            fmt.Println("No tasks are ready yet")
        }
    }

    Output:

    No tasks are ready yet

    Infinite Blocking without Cases: select statement with no cases will block indefinitely. This is commonly used when you need an infinite wait.

    package main
    
    func main() {
        select {} // This blocks indefinitely because no cases are present
    }

    Output:

    Welcome to Go Programming
    Channels in Go Language

    channel in Go is a medium that enables communication between goroutines without using explicit locks. Channels facilitate the exchange of data between goroutines in a synchronized manner, and by default, they are bidirectional. This means the same channel can be used for both sending and receiving data. Below is a detailed explanation and examples of how channels work in Go.

    Creating a Channel; In Go, you use the chan keyword to create a channel. A channel can only transport data of a specific type, and you cannot use the same channel to transfer different data types.

    Syntax:

    var channelName chan Type

    Example:

    // Go program to demonstrate channel creation
    package main
    
    import "fmt"
    
    func main() {
        // Creating a channel using var
        var myChannel chan string
        fmt.Println("Channel value:", myChannel)
        fmt.Printf("Channel type: %T\n", myChannel)
    
        // Creating a channel using make()
        anotherChannel := make(chan string)
        fmt.Println("Another channel value:", anotherChannel)
        fmt.Printf("Another channel type: %T\n", anotherChannel)
    }

    Output:

    Channel value: <nil>
    Channel type: chan string
    Another channel value: 0xc00007c060
    Another channel type: chan string
    Sending and Receiving Data in a Channel

    Channels in Go operate through two primary actions: sending and receiving, collectively referred to as communication. These operations use the <- operator to indicate the direction of the data flow.

    1. Sending Data: The send operation transfers data from one goroutine to another via a channel. For basic data types like integers, floats, and strings, sending is straightforward and safe. However, when working with pointers or references (like slices or maps), ensure that only one goroutine accesses them at a time.

    myChannel <- value

    2. Receiving Data: The receive operation fetches the data from a channel that was sent by another goroutin

    var channelName chan Type

    Example:

    // Go program to demonstrate send and receive operations
    package main
    
    import "fmt"
    
    func calculateSquare(ch chan int) {
        num := <-ch
        fmt.Println("Square of the number is:", num*num)
    }
    
    func main() {
        fmt.Println("Main function starts")
    
        // Creating a channel
        ch := make(chan int)
    
        go calculateSquare(ch)
    
        ch <- 12
    
        fmt.Println("Main function ends")
    }

    Output:

    Main function starts
    Square of the number is: 144
    Main function ends

    Closing a Channel: You can close a channel using the close() function, which indicates that no more data will be sent to that channel.

    Syntax:

    close(channelName)

    When iterating over a channel using a for range loop, the receiver can determine if the channel is open or closed.

    Syntax:

    value, ok := <-channelName

    If ok is true, the channel is open, and you can read data. If ok is false, the channel is closed.

    Example:

    // Go program to close a channel using for-range loop
    package main
    
    import "fmt"
    
    func sendData(ch chan int) {
        for i := 1; i <= 5; i++ {
            ch <- i
        }
        close(ch)
    }
    
    func main() {
        ch := make(chan int)
    
        go sendData(ch)
    
        for value := range ch {
            fmt.Println("Received value:", value)
        }
        fmt.Println("Channel closed")
    }

    Output:

    Received value: 1
    Received value: 2
    Received value: 3
    Received value: 4
    Received value: 5
    Channel closed
    Key Points to Remember
    1. Blocking Behavior:
      • Sending data blocks until another goroutine is ready to receive it.
      • Receiving data blocks until another goroutine sends it.
    2. Nil Channels:
      • A zero-value channel is nil, and operations on it will block indefinitely.
    3. Iterating Over Channels:
      • for range loop can iterate over the values in a channel until it’s closed.

    Example:

    // Go program to iterate over channel using for-range
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan string)
    
        go func() {
            ch <- "Hello"
            ch <- "World"
            ch <- "Go"
            close(ch)
        }()
    
        for msg := range ch {
            fmt.Println(msg)
        }
    }

    Output:

    Hello
    World
    Go
    Channel Properties

    1.Length of a Channel: Use len() to find the number of elements currently in the channel.

    Example:

    // Go program to find channel length
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
    
        fmt.Println("Channel length:", len(ch))
    }

    Output:

    Channel length: 2

    2. Capacity of a Channel: Use cap() to find the total capacity of the channel.

    Example:

    // Go program to find channel capacity
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan float64, 4)
    
        ch <- 1.1
        ch <- 2.2
    
        fmt.Println("Channel capacity:", cap(ch))
    }

    Output:

    Channel capacity: 4

    In Golang, channels act as a communication mechanism between concurrently running goroutines, allowing them to transmit and receive data. By default, channels in Go are bidirectional, meaning they support both sending and receiving operations. However, it’s possible to create unidirectional channels, which can either exclusively send or receive data. These unidirectional channels can be constructed using the make() function as demonstrated below:

    // For receiving data only
    c1 := make(<-chan bool)
    
    // For sending data only
    c2 := make(chan<- bool)

    Example:

    // Go program to demonstrate the concept
    // of unidirectional channels
    package main
    
    import "fmt"
    
    func main() {
        // Channel restricted to receiving data
        recvOnly := make(<-chan int)
    
        // Channel restricted to sending data
        sendOnly := make(chan<- int)
    
        // Display the types of the channels
        fmt.Printf("%T", recvOnly)
        fmt.Printf("\n%T", sendOnly)
    }

    Output:

    <-chan int
    chan<- int
    Converting Bidirectional Channels into Unidirectional Channels

    In Go, you can convert a bidirectional channel into a unidirectional channel, meaning you can restrict it to either sending or receiving data. However, the reverse conversion (from unidirectional back to bidirectional) is not possible. This concept is illustrated in the following example:

    Example:

    // Go program to illustrate conversion
    // of a bidirectional channel into a
    // unidirectional channel
    package main
    
    import "fmt"
    
    // Function to send data through a send-only channel
    func sendData(channel chan<- string) {
        channel <- "Hello from Golang"
    }
    
    func main() {
        // Creating a bidirectional channel
        bidiChannel := make(chan string)
    
        // Passing the bidirectional channel to a function,
        // which restricts it to a send-only channel
        go sendData(bidiChannel)
    
        // Receiving data from the channel
        fmt.Println(<-bidiChannel)
    }

    Output:

    Hello from Golang

  • Pointers in Go (Golang)

    A pointer is a variable that stores the memory address of another variable. In Go, pointer types look like *int, *string, *MyStruct, etc.

    • &x → “address of x”
    • *p → “value at the address stored in p” (dereference)

    Why pointers are used

    Pointers are useful when you want:

    • To modify a variable inside a function (pass-by-reference style)
    • To avoid copying large structs (performance)
    • To share one piece of data across multiple functions safely (with proper synchronization)

    Basic pointer example (address + dereference)

    package main
    
    import "fmt"
    
    func main() {
        num := 60
        ptr := &num // ptr stores address of num
    
        fmt.Println("num:", num)
        fmt.Println("&num:", &num)
        fmt.Println("ptr:", ptr)
        fmt.Println("*ptr:", *ptr) // dereference
    
        *ptr = 90 // modify through pointer
        fmt.Println("updated num:", num)
    }
    

    Nil pointer

    Uninitialized pointers are nil.

    var p *int
    fmt.Println(p) // <nil>
    

    You must not dereference nil (*p) — it will panic.


    Passing pointers to functions (modify original)

    Method 1: Pass a pointer variable

    func modifyValue(p *int) {
        *p = 1234
    }
    
    func main() {
        num := 42
        ptr := &num
        modifyValue(ptr)
        fmt.Println(num) // 1234
    }
    

    Method 2: Pass address directly

    modifyValue(&num)
    

    Pointers with structs

    Using & (address of an existing struct)

    type Book struct {
        title  string
        author string
    }
    
    func main() {
        b := Book{title: "Go Programming", author: "Alice"}
        pb := &b
    
        pb.title = "Advanced Go" // auto-dereference for fields
        fmt.Println(b.title)     // Advanced Go
    }
    

    Using new

    new(T) allocates zero-value T and returns *T.

    type Car struct {
        model string
        year  int
    }
    
    func main() {
        pc := new(Car)
        pc.model = "Tesla Model 3"
        pc.year = 2023
    }
    

    Pointer to pointer (**T) — “double pointer”

    A pointer can also point to another pointer.

    package main
    
    import "fmt"
    
    func main() {
        number := 500
        p1 := &number
        p2 := &p1 // pointer to pointer
    
        fmt.Println(number) // 500
        fmt.Println(*p1)    // 500
        fmt.Println(**p2)   // 500
    
        **p2 = 1000
        fmt.Println(number) // 1000
    }
    

    Comparing pointers

    You can compare pointers using == and !=.

    • Two pointers are equal if they point to the same address, or both are nil.

    ✅ Correct comparison (compare pointer values):

    p1 := &a
    p2 := &b
    p3 := &a
    
    fmt.Println(p1 == p2) // false
    fmt.Println(p1 == p3) // true
    

    ⚠️ Note: comparing &p1 == &p2 compares the addresses of the pointer variables themselves, not what they point to.

  • File Handling (Input/Output) in Go (Golang)

    Introduction to File Handling

    File handling refers to the process of creating, reading, writing, updating, and deleting files stored on a computer’s file system. In Go, file handling is part of standard input/output (I/O) operations and is handled mainly using the os, io, and bufio packages.

    File handling allows programs to:

    • Store data permanently
    • Read configuration files
    • Process logs
    • Handle large datasets
    • Exchange data between programs

    Packages Used for File Handling in Go

    Go provides powerful built-in packages for file I/O:

    PackagePurpose
    osFile creation, opening, deleting
    ioLow-level I/O primitives
    bufioBuffered I/O (efficient reading/writing)
    fmtFormatted input/output
    ioutil (deprecated)Older file utilities (replaced by os & io)

    Creating a File in Go

    Using os.Create()

    os.Create() creates a new file.
    If the file already exists, it truncates (clears) the file.

    package main
    
    import (
    	"os"
    )
    
    func main() {
    	file, err := os.Create("example.txt")
    	if err != nil {
    		panic(err)
    	}
    	defer file.Close()
    }
    

    Explanation

    • Returns a file pointer
    • Must always close the file
    • defer file.Close() ensures cleanup

    Opening an Existing File

    Using os.Open()

    Used for read-only access.

    file, err := os.Open("example.txt")
    if err != nil {
    	panic(err)
    }
    defer file.Close()
    

    Using os.OpenFile()

    Allows full control over file access modes.

    file, err := os.OpenFile(
    	"example.txt",
    	os.O_APPEND|os.O_CREATE|os.O_WRONLY,
    	0644,
    )
    

    File Flags

    FlagMeaning
    os.O_RDONLYRead only
    os.O_WRONLYWrite only
    os.O_RDWRRead & write
    os.O_CREATECreate if not exists
    os.O_APPENDAppend to file
    os.O_TRUNCTruncate file

    Writing to a File

    Writing Using WriteString()

    file, _ := os.Create("data.txt")
    defer file.Close()
    
    file.WriteString("Hello, Go File Handling\n")
    

    Writing Using fmt.Fprintln()

    fmt.Fprintln(file, "This is a line written using fmt")
    

    Writing Using bufio.Writer (Recommended)

    Buffered writing is faster.

    writer := bufio.NewWriter(file)
    writer.WriteString("Buffered writing in Go\n")
    writer.Flush()
    

    Reading from a File

    Reading Using os.ReadFile() (Simple)

    data, err := os.ReadFile("example.txt")
    if err != nil {
    	panic(err)
    }
    fmt.Println(string(data))
    

    Best for small files.


    Reading Using bufio.Scanner (Line by Line)

    file, _ := os.Open("example.txt")
    defer file.Close()
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
    	fmt.Println(scanner.Text())
    }
    

    Used for:

    • Log files
    • Text processing
    • Line-based input

    Reading Using bufio.Reader

    reader := bufio.NewReader(file)
    line, _ := reader.ReadString('\n')
    fmt.Println(line)
    

    Appending to a File

    Appending adds content without deleting existing data.

    file, _ := os.OpenFile(
    	"example.txt",
    	os.O_APPEND|os.O_WRONLY,
    	0644,
    )
    defer file.Close()
    
    file.WriteString("New appended line\n")
    

    Deleting a File

    Use os.Remove().

    err := os.Remove("example.txt")
    if err != nil {
    	panic(err)
    }
    

    Checking if a File Exists

    if _, err := os.Stat("example.txt"); err == nil {
    	fmt.Println("File exists")
    } else {
    	fmt.Println("File does not exist")
    }
    

    Getting File Information

    Use os.Stat().

    info, _ := os.Stat("example.txt")
    
    fmt.Println("Name:", info.Name())
    fmt.Println("Size:", info.Size())
    fmt.Println("Permissions:", info.Mode())
    fmt.Println("Modified:", info.ModTime())
    

    File Permissions in Go

    Permissions use Unix-style notation.

    ValueMeaning
    0644Owner read/write, others read
    0755Executable permissions
    0600Owner only

    Handling Errors in File I/O

    Error handling is critical in Go.

    file, err := os.Open("missing.txt")
    if err != nil {
    	fmt.Println("Error:", err)
    	return
    }
    

    Go forces explicit error checking, improving reliability.


    Copying File Contents

    source, _ := os.Open("a.txt")
    defer source.Close()
    
    destination, _ := os.Create("b.txt")
    defer destination.Close()
    
    io.Copy(destination, source)
    

    Reading User Input and Writing to File

    reader := bufio.NewReader(os.Stdin)
    fmt.Print("Enter text: ")
    input, _ := reader.ReadString('\n')
    
    file, _ := os.Create("user.txt")
    defer file.Close()
    
    file.WriteString(input)
    

    File Handling Best Practices

    • Always close files
    • Use defer
    • Prefer buffered I/O
    • Handle errors properly
    • Avoid reading large files fully into memory

    Common Mistakes in Go File Handling

    • Forgetting file.Close()
    • Ignoring errors
    • Using deprecated ioutil package
    • Overwriting files unintentionally
    • Not flushing buffered writers

    Practical Example: Log File Writer

    file, _ := os.OpenFile("log.txt",
    	os.O_APPEND|os.O_CREATE|os.O_WRONLY,
    	0644)
    
    defer file.Close()
    
    log := bufio.NewWriter(file)
    log.WriteString("Application started\n")
    log.Flush()
    

    Summary

    • Go provides robust file I/O using os, io, and bufio
    • Supports file creation, reading, writing, appending, and deletion
    • Explicit error handling ensures safety
    • Buffered I/O improves performance
    • Essential for backend systems, CLI tools, and system programs

  • Slices in Golang

    Slices

    What Are Slices?
    • Slices are a flexible and dynamic abstraction over arrays in Go.
    • They represent a contiguous segment of an array and include a pointerlength, and capacity.
    • Unlike arrays, slices are resizable.
    Key Features:
    1. Dynamic Size: Unlike arrays, slices can grow or shrink as required.
    2. Reference Type: Slices point to an underlying array. Changes to the slice affect the array and vice versa.
    3. Homogeneous Elements: Slices can only hold elements of the same type.
    4. Support for Duplicates: Slices can contain duplicate elements.
    Slice Components:
    1. Pointer: Points to the starting element of the slice.
    2. Length: Number of elements in the slice.
    3. Capacity: Maximum number of elements the slice can accommodate without reallocation.
    Creating Slices:

    1. From Arrays: Use slicing syntax array[low:high].

    2. Slice Literals:

    mySlice := []int{1, 2, 3}

    3. Using make():

    mySlice := make([]int, length, capacity)

    4. From Existing Slices: Use slicing syntax on slices.

    Array elements:
    10
    20
    30
    40
    50

    Examples:

    1. Basic Slicing:

    arr := [5]int{1, 2, 3, 4, 5}
    slice := arr[1:4]
    fmt.Println(slice) // Output: [2 3 4]

    2: Using append():

    slice := []int{1, 2}
    slice = append(slice, 3, 4)
    fmt.Println(slice) // Output: [1 2 3 4]

    3. Using make():

    slice := make([]int, 3, 5)
    fmt.Println(slice) // Output: [0 0 0]

    4. Iterating Over Slices:

    • Using for loop:
    for i := 0; i < len(slice); i++ {
        fmt.Println(slice[i])
    }
    • Using range:
    for idx, val := range slice {
        fmt.Printf("Index: %d, Value: %d\n", idx, val)
    }

    Output:

    Colors in the array:
    Red
    Green
    Blue

    Slice Composite Literal in Go

    There are two important terms: Slice and Composite Literal. A slice is a composite data type similar to an array, used to store multiple elements of the same data type. The key distinction between an array and a slice is that a slice can adjust its size dynamically, whereas an array cannot.

    On the other hand, Composite Literals are utilized to create values for slices, arrays, structs, and maps. Each time a composite literal is evaluated, a fresh value is created. They consist of the type of the literal followed by a brace-enclosed list of elements. After reading this explanation, you’ll likely recognize composite literals and be surprised to find that you already understand them!

    // Go program to demonstrate a slice
    // using a composite literal
    package main
    
    import "fmt"
    
    func main() {
    
        // Creating a slice with a composite literal
        // A slice groups together values of the same type
        // Here, the values are of type float64
        nums := []float64{3.14, 2.71, 1.41, 0.577}
    
        // Displaying the slice values
        fmt.Println(nums)
    }

    Output:

    [3.14 2.71 1.41 0.577]

    Slices created using composite literals are a shorthand way to initialize or assign values to slices, arrays, etc. These literals are especially handy for grouping values of similar types.

    slice composite literal in Go provides a concise syntax for creating slices by explicitly listing their elements. The syntax looks like []T{e1, e2, …, ek}, where T is the element type, and e1, e2, …, ek are the slice elements.

    Let’s see another example of a slice composite literal in Go:

    package main
    
    import "fmt"
    
    func main() {
        // Initializing a slice with integers
        numbers := []int{10, 20, 30, 40, 50}
    
        // Printing the slice
        fmt.Println("Numbers slice:", numbers)
    }

    Output:

    Numbers slice: [10 20 30 40 50]

    In this case, the composite literal []int{10, 20, 30, 40, 50} creates a slice containing the integers 10, 20, 30, 40, and 50. Since the element type is int, the slice type becomes []int.

    You can also use slice composite literals for other data types like strings, float64, or even custom types. The syntax remains consistent, and the elements within the slice must share the same type.

    Here’s an example of a slice composite literal with string elements:

    package main
    
    import "fmt"
    
    func main() {
        // Creating a slice of strings
        fruits := []string{"mango", "grape", "peach"}
    
        // Displaying the string slice
        fmt.Println("Fruits slice:", fruits)
    }

    Output:

    Fruits slice: [mango grape peach]
  • Arrays in Go

    Arrays

    Understanding Arrays in Go Programming Language

    Arrays in Go (or Golang) are conceptually similar to arrays in other programming languages. They are particularly useful when you need to manage a collection of data of the same type, such as storing a list of student scores. An array is a fixed-length data structure used to store homogeneous elements in memory. However, because arrays have a fixed size, slices are often preferred in Go.

    Key Features of Arrays in Go

    • Arrays are fixed in size, meaning their length cannot change once defined.
    • Arrays allow zero or more elements of the same type.
    • Array elements are accessed using their zero-based index, with the first element at index array[0] and the last at array[len(array)-1].
    • Arrays are mutable, allowing modification of elements by their index.
    Creating and Accessing Arrays in Go

    Using the var Keyword

    Arrays in Go can be declared using the var keyword.

    Syntax:

    var array_name [length]Type

    Key Points

    • Arrays are mutable, so you can update their elements using the index
    var array_name [length]Type
    array_name[index] = value
    • Access elements using their index or iterate through the array using a loop.
    • Arrays in Go are one-dimensional by default.
    • Duplicate elements are allowed.
    package main
    
    import "fmt"
    
    func main() {
        var numbers [5]int
        numbers[0] = 10
        numbers[1] = 20
        numbers[2] = 30
        numbers[3] = 40
        numbers[4] = 50
    
        fmt.Println("Array elements:")
        for i := 0; i < len(numbers); i++ {
            fmt.Println(numbers[i])
        }
    }

    Output:

    Array elements:
    10
    20
    30
    40
    50

    Using Shorthand Declaration

    A shorthand declaration allows you to define and initialize an array in a single line.

    Syntax:

    array_name := [length]Type{element1, element2, ..., elementN}

    Example 2: Shorthand Declaration

    package main
    
    import "fmt"
    
    func main() {
        colors := [3]string{"Red", "Green", "Blue"}
    
        fmt.Println("Colors in the array:")
        for _, color := range colors {
            fmt.Println(color)
        }
    }

    Output:

    Array elements:
    10
    20
    30
    40
    50

    Using Shorthand Declaration: A shorthand declaration allows you to define and initialize an array in a single line.

    Syntax:

    array_name := [length]Type{element1, element2, ..., elementN}

    Example 2: Shorthand Declaration

    package main
    
    import "fmt"
    
    func main() {
        colors := [3]string{"Red", "Green", "Blue"}
    
        fmt.Println("Colors in the array:")
        for _, color := range colors {
            fmt.Println(color)
        }
    }

    Output:

    Colors in the array:
    Red
    Green
    Blue
    Multi-Dimensional Arrays

    In Go, arrays are one-dimensional, but you can create multi-dimensional arrays (arrays of arrays). These arrays allow you to store data in tabular or matrix format.

    Syntax

    var array_name [Length1][Length2]...[LengthN]Type

    Example:

    package main
    
    import "fmt"
    
    func main() {
        matrix := [2][2]int{{1, 2}, {3, 4}}
    
        fmt.Println("Matrix elements:")
        for i := 0; i < 2; i++ {
            for j := 0; j < 2; j++ {
                fmt.Printf("%d ", matrix[i][j])
            }
            fmt.Println()
        }
    }

    Output:

    Matrix elements:
    1 2
    3 4

    How to Copy an Array into Another Array in Golang?

    In Go, an array is a fixed-size data structure that stores elements of the same type. Unlike slices, arrays have a predefined size that cannot be changed after declaration. Copying one array into another is straightforward but requires both arrays to have the same length and type.

    Syntax:

    for i := 0; i < len(sourceArray); i++ {
        targetArray[i] = sourceArray[i]
    }

    Example:

    package main
    
    import "fmt"
    
    // Array used for demonstration
    var sourceArray = [5]int{1, 2, 3, 4, 5}
    
    func main() {
        fmt.Println("Original Array:", sourceArray)
    }

    Output:

    Product of 2, 3, 4: 24
    Product of 6, 7: 42
    Product with no numbers: 1
    1. Using a Loop to Copy an Array

    Go does not have a built-in copy() function for arrays. The most common method involves manually iterating over each element of the source array and copying it to the target array.

    Syntax

    for i := 0; i < len(sourceArray); i++ {
        targetArray[i] = sourceArray[i]
    }

    Example:

    package main
    
    import "fmt"
    
    // Predefined source array
    var sourceArray = [5]int{1, 2, 3, 4, 5}
    
    func main() {
        // Initialize target array with the same size as the source
        var targetArray [5]int
    
        // Copy each element manually
        for i := 0; i < len(sourceArray); i++ {
            targetArray[i] = sourceArray[i]
        }
    
        fmt.Println("Source Array:", sourceArray)
        fmt.Println("Target Array:", targetArray)
    }

    Output:

    Source Array: [1 2 3 4 5]
    Target Array: [1 2 3 4 5]

    2. Direct Assignment (Specific to Arrays, Not Applicable to Slices)

    In Go, arrays can be directly assigned to another array if their type and length are identical. This provides a simpler way to copy arrays compared to looping.

    Syntax:

    targetArray = sourceArray

    Example:

    package main
    
    import "fmt"
    
    // Source array for demonstration
    var sourceArray = [5]int{6, 7, 8, 9, 10}
    
    func main() {
        // Assign source array to the target array
        var targetArray [5]int = sourceArray
    
        fmt.Println("Source Array:", sourceArray)
        fmt.Println("Target Array:", targetArray)
    }

    Output:

    Source Array: [6 7 8 9 10]
    Target Array: [6 7 8 9 10]
    3. Using Pointers for Large Arrays

    When dealing with large arrays, it is more efficient to use pointers to reference the memory location of the source array instead of copying all its elements. This approach allows direct manipulation of the array data without duplicating it.

    Syntax

    targetPointer = &sourceArray

    Example:

    package main
    
    import "fmt"
    
    // Example source array
    var sourceArray = [5]int{11, 12, 13, 14, 15}
    
    func main() {
        // Create a pointer to the source array
        var targetPointer *[5]int = &sourceArray
    
        fmt.Println("Source Array:", sourceArray)
        fmt.Println("Target Array via Pointer:", *targetPointer)
    }

    Output:

    Source Array: [11 12 13 14 15]
    Target Array via Pointer: [11 12 13 14 15]