Golang notes no. 1

May 22, 2023

  • Arrays

    There are some big differences between how arrays work in Go and C. In Go,

Arrays are like values. When you assign one array to another, all the elements get copied.

If you pass an array to a function in Go, the function gets its own copy of the array, not a pointer to it.

The size of an array is part of its type. So, [10]int and [20]int are considered different types.

Slices are a way to work with arrays that's more flexible, powerful, and convenient. Slices actually refer to an underlying array, and when you assign one slice to another, they both point to the same array. If you change elements in a slice passed to a function, those changes will be visible to the caller, just like passing a pointer to the original array. So, instead of passing a pointer and a count, you can simply pass a slice to a Read function, and the length of the slice determines how much data can be read.

  • pointer method vs value method

This rule is all about the difference between pointer methods and value methods in Go. Basically, if a method has a pointer receiver, it can change the original variable it's called on. But if it has a value receiver, it can only modify a copy of that variable. Now, here's the tricky part: if you try to call a pointer method on a value, the value gets copied and the method is applied to that copy. But since the copy gets thrown away, any changes made to the receiver are pointless.

To prevent this confusion, Go doesn't allow calling pointer methods on values, unless you're dealing with an addressable value. In that case, the Go compiler steps in and automatically adds the & address operator to the value, turning it into a pointer. That's exactly what happens in the example they gave. The variable b is addressable, so you can simply call b.Write instead of (&b).Write. Behind the scenes, the compiler cleverly rewrites it as (&b).Write, ensuring that the method works on a pointer and not just a value. Sneaky, huh?

  • Data allocation

So, there's this nifty function in Go called new(). It's pretty cool because it helps you allocate memory. But here's the thing, unlike its buddies in some other programming languages, it doesn't actually initialize the memory for you. It just zeroes it out. Yep, that's it! When you call new(T), it allocates memory and gives you the address of the newly allocated zeroed storage for an item of type T. In other words, it hands you a pointer (*T) to a fresh new zero value of type T. That's how Go rolls!

Now, here comes the interesting part. This zero-value thingy is pretty handy and works like magic. Let's say we have this type called SyncedBuffer with a couple of fields in it, like a lock of type sync.Mutex and a buffer of type bytes.Buffer. When you allocate or declare a value of type SyncedBuffer, you can use it right away without any extra setup. Check out this code snippet:

p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer

By calling new(SyncedBuffer), you create a pointer to a freshly allocated zero value of type SyncedBuffer. On the other hand, when you do var v SyncedBuffer, you directly create a value of type SyncedBuffer. The difference is that p is a pointer to the allocated value, while v is the value itself. So, when you use p, you need to use the dereference operator (*p) to access the actual value, while with v, you can use it directly.

Here's another tidbit: when you pass p as an argument to a function, you're passing a reference to the value. Any changes made to the value inside the function will be visible outside too. But with v, the function receives a copy of the value. So any changes made to it won't affect the original value outside the function.

Oh, and here's another topic: composite literals! You can use them to initialize a struct in a nice and concise way. Check this out:

f := File{fd, name, nil, 0}
return &f

By using File{fd, name, nil, 0}, you're creating a composite literal for the File struct. It assigns values to the struct's fields in order. Pretty handy when you want to quickly initialize a struct with some values.

Now, let's move on to the make() function. This one is different from new(). It's used specifically for creating slices, maps, and channels, and it returns an initialized value (not zeroed!). You see, slices, maps, and channels are special types that actually represent references to data structures. These data structures need to be properly initialized before you can use them. That's why make() exists to take care of that initialization for you.

  • Parallelism

So, we have two terms here: concurrency and parallelism. They may sound similar, but they're actually different concepts. Concurrency is all about structuring your program into independently executing components. It's like juggling multiple tasks at the same time, where each task does its thing without waiting for others. On the other hand, parallelism is about running calculations simultaneously on multiple CPUs to improve efficiency. It's like having multiple workers collaborating on different parts of a big task.

Now, here's the deal with Go. Go is a language that's really good at handling concurrency. It provides some awesome features that make it easy to structure your program in a concurrent way. These features can help you solve problems by breaking them down into independent parts that can execute concurrently. So, in a way, Go is like your buddy who's great at multitasking and keeping things organized.

But here's the catch: Go is not primarily designed for parallelism. It's not all about spreading calculations across multiple CPUs for maximum speed. It doesn't have all the fancy tools and optimizations specifically built for parallel execution. So, not every parallelization problem fits nicely into Go's model. It's like expecting your buddy who's good at multitasking to suddenly become a super-efficient worker in a factory assembly line. It's just not their specialty.

So, to sum it up, Go is a kick-ass language for handling concurrency. It excels at structuring programs with independent components. But when it comes to parallelism—running calculations in parallel on multiple CPUs—Go isn't the go-to choice. It's more about getting things done concurrently rather than unleashing the full power of parallel execution.

  • Interfaces

Imagine you're running a shop, and you store customer and sales data in a PostgreSQL database. Now, you want to write some code that calculates the sales rate (sales per customer) for the past 24 hours, rounded to 2 decimal places.

To get started, you write a basic implementation that connects to the database, queries the necessary data, and performs the calculations. It works fine, but there's one issue. Testing this code is a bit of a hassle. You need to set up a test database with dummy data and handle all the setup and teardown tasks. It's a lot of work for just testing the math logic!

So, what's the solution? Interfaces to the rescue!

You can create your own interface type that describes the methods your calculateSalesRate() function relies on, like CountCustomers() and CountSales(). Then, you update the signature of calculateSalesRate() to accept this custom interface type as a parameter instead of the concrete *ShopDB type.

By doing this, you've made your code more flexible and testable. Now, you can create a mock object that implements your custom interface and use it during unit tests. This mock object provides the necessary data without the need for a real database. You can control the inputs and check if the math logic in calculateSalesRate() produces the expected results.

To sum it up, interfaces help in a few ways: they reduce duplication in code, enforce decoupling between different parts of your codebase, and make it easier to use mock objects instead of real ones in unit tests. In the case of our shop example, interfaces allow us to create a mock object that satisfies the interface and simplifies the testing process.

So, with interfaces, you can write more flexible and maintainable code, and easily test different scenarios without the need for complex setups. It's like having a toolbox full of handy tools to make your code more robust and your life as a developer easier.

Empty Interface

So, there's this special type in Go called the "empty interface," denoted by interface{}. It's kind of like a wildcard that can represent any type. You can use it when you want to work with objects of different types without restricting yourself to a specific one.

In the code example, we have a person map that uses the empty interface as the value type. This allows us to store values of different types in the same map. For instance, we add a name (a string), age (an integer), and height (a float) to the person map.

Now, here's an interesting part. Let's say we want to increment the person's age by 1. If we try to do person["age"] = person["age"] + 1, we'll encounter an error: "invalid operation: person["age"] + 1 (mismatched types interface {} and int)."

Why does this happen? Well, when we retrieve a value from the map, it becomes of type interface{}. It loses its original type information (in this case, int). Since it's no longer considered an int, we can't directly add 1 to it.

To work around this, we need to perform a type assertion to convert the value back to its original type (in this case, int). We can use the syntax age, ok := person["age"].(int) to do this. The type assertion returns two values: the value of the asserted type (age in this case) and a boolean flag (ok) indicating whether the assertion succeeded.

If the assertion fails (i.e., ok is false), it means the value in the map couldn't be converted to the desired type. In that case, we can handle the error accordingly.

With the type assertion in place, we can successfully increment the age by doing person["age"] = age + 1. Now, the age value is treated as an int, and we can perform arithmetic operations on it.

It's worth mentioning that using concrete types or non-empty interfaces is generally better in terms of clarity, safety, and performance. However, the empty interface comes in handy when you're dealing with unpredictable or user-defined types. In fact, you'll often encounter it in various places throughout the Go standard library, such as in functions like gob.Encode, fmt.Print, and template.Execute.

So, while the empty interface should be used judiciously, it's a useful tool when you need the flexibility to work with different types in certain scenarios.