Diving into Golang p1 - First Impressions 2023-12-03

Just a heads up, this is not to be taken as a tutorial or reference. If any code in this is helpful or points something out that's great, however this is just my experience so far with Go. Lets start with a few questions. Why Go? What are your goals for this language? How are you going to teach yourself the language?

Why Go?

Ive been hesitant to learn a new language for a long time now. Ive gotten by with Python, C#, C, and the little JS (frontend and a little Node) and LUA (which I mostly just use for plugins or api's like the Minecraft computer craft mod) I know. Ive been observing the landscape of languages and I think its cool that a lot of modern systems languages are popping up, and some are trying to work with what we already have like Groovy and Kotlin taking advantage of the JVM, but I don't like learning something if I don't have a good reason to (this applies to a lot of things), so what are my reasons?

I've been finding myself working on or even planning a lot of networking applications. I enjoy writing servers and clients and I really think Go is a network developer's dream language. You've probably heard some of this before so Im just gonna shoot it out quick.

Its fast, I am able to use pointers and in the event I need more I have access to the Unsafe package, its standard lib has enough features for the language to stand on its own, its syntax is kinda if you mixed Python and C (which are two languages I love the syntax of), and it feels so different having so many interesting approaches to problems or features while being pretty straight forward and removing the "magic" that I truly hate that plagues C# and a lot of modern languages.

This is an important point so I want to go into it a bit more. I love C# as a language, it has a lot of great features, and its really powerful, but a lot of the time I'm learning more about C# the more it relies on "magic" where it does something but I cant really tell what its doing or how it is doing it since its behind so many layers of abstraction. Which seems to be how the devs for C# want you to write C# code. I feel like the biggest offender of this aside from JavaScript and seemingly every framework that gets produced by that community is C#'s ASP.NET. ASP.NET was my first experience learning about backend web development and I understood none of it. instead of teaching me about how HTTP works and how we interact with the requests I had to try to understand MVC and how it works, and when I finally got to the part of the course that was teaching me about retrieving data from a post request it just said something along the lines of "we don't need to worry about that since we just call this method and it takes care of it for us" I fundamentally hate this mindset. Part of the reason I program is to learn how our technology works, not hide behind it and just trust it. That's why I love Go and all the code Ive read for Go. Its straight forward and I can read Go code and have at least a decent understanding on how it works. I cannot say the same thing about ASP.NET, the code is too abstracted and I genuinely have no idea where anything is happening or what its doing. I don't want magic I just want to be able to read and write something that doesn't require someone to go to the documentation to understand. In base C# I feel like that problem isn't as common but still ends up happening. I still love C# but this is just a gripe that has annoyed me over time.

I understand Go has some downsides. It has a garbage collector and that can occasionally get in the way of performance, Its still a newer language so there isn't as much in the way of documentation or libraries/frameworks, and its binary/exe sizes are pretty big in comparison to C/C++. I'm not going into the language without that knowledge, but In comparison with what I'm getting I don't really care if that's the case.

Final note on this question. Why wasn't C++ or Rust chosen? In the case of C++ I don't mind the language but I'm trying to move away from an object oriented approach, and C++ is a Frankenstein of different features and standards I don't want to touch. Rust is a language I don't really care for which I know the subject of Rust can be divisive, but I don't really care for how it handles its syntax, memory management, and its community is something I try to stay away from, since in my experience Rust devs have been really toxic and pushy especially on the subject of the language itself.

What are my goals for this language?

My main goal is for Go to be the primary language I write all my projects that center around networking, the web, and automation.

How are you going to teach yourself the language?

Honestly I'm just going to build some small projects to build a general understanding of the syntax, explore the standard library, and adjust to the official documentation and the third party library documentation, such as the driver for PostgreSQL. I'm also using Go By Example as a reference while I am looking at things for the first time.

The project

The first project I'm going to tackle is a db application that handles data for cooking recipes since Ive been cooking more and I need to track all the recipes Ive been making in one spot. I also have some cool ideas for it later down the line, but for now its going to do the following. take input of a recipe through a JSON file, get all the data from the JSON, store it in either a sqlite or psql database, and retrieve the data from the database. Very simple but that's the point. I'm just trying to learn the syntax and how the language wants me to think. This is very important since I am used to object oriented languages (with the exception of C but it should assist my C thought processes too since they are both procedural languages that rely on pointers & data structures). Just to note I don't plan on finishing the project in this post I just want to get the database setup, inserting data, retrieving data, and importing/exporting JSON data to/from a file.

Initial Impressions

To start off I enjoy that though the language is compiled I can still run the program in a similar method as python. Python will be the comparison I use the most since its the language I am most familiar with.

Where Python would run code with python script.py I can just use go run main.go. which is a feature that I value a great deal since in some compiled languages such as C/C++ I have to compile each time I want to run the code. Just like Python Go has its own way of retrieving libraries using the go get command. The tooling that Go provides so far is checking all the boxes I need it to check, I will visit compiling and cross compiling at the end of the project.

I need PostgreSQL for the initial half of the project so I am going to be using the github.com/lib/pq package.

An earlier version of this post used SQLite and so the following paragraph is from looking at the docs for go-sqlite3. It's taking me a minute to read and understand the docs. I don't know if this is something that is exclusive with 3rd party libraries or not but I am able to understand what the docs are telling me if they are usually accompanied by some sort of usage example like how Python's SQLite documentation. Which is weird because I'm finding that the VSCode Intellisense has more helpful explanations than the actual library documentation.

package main

import (
    "database/sql"
    "fmt"

    _ "github.com/lib/pq"
)

const (
    host     = "192.168.1.52" //server address
    port     = 5432 // psql default port
    user     = "postgres" // dummy user non-admin, only usable in this db
    dbname   = "recipedump" // created a db ahead of time on the server
    password = "" //intentionally blank, hardcoding with dummy user only for purposes of testing. 
)

func CreateDb(db *sql.DB) {
    _, err := db.Exec(`
        CREATE TABLE IF NOT EXISTS recipes(
            "id" SERIAL PRIMARY KEY,
            "name" text,
            "ingredients" text[],
            "steps" text
    );`)

    if err != nil {
        panic(err)
    } else {
        fmt.Println("Created database")
    }
}

func main() {

    var psqlData string = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)

    db, err := sql.Open("postgres", psqlData)
    if err != nil {
        panic(err)
    }

    CreateDb(db)

    defer db.Close()
}

Here is the initial code I wrote that ended up working. Initially I was getting errors but wasn't sure why but its because I never stored the error output of the function db.Exec(new_row) into a variable. After I realized that it without question made it easier to understand what was happening, Ill blame that one on me. Good Segway! Ive heard a lot of anguish over the way go handles error handling with the following block of code.

if err != nil {
    panic(err)
}

I don't know about you but I enjoy this. To me this is much more straight forward and easy to understand, and I can understand where people are coming from however I feel like the argument doesn't have too much room to stand. Where Exception handing using try in languages to me just adds an unnecessary indentation, and in the end most people just end up using the generic Exception that captures all exceptions, which is the same function as the snippet above. Go is also forcing you to think about what to do if something goes wrong which I feel like is something we all should do. Not to say that it all should be handled like the above or in their own specific ways but just the thought of "Hey this process failed. What should I do in the event this actually happens", but in the end we all handle errors in our own ways. Some people don't even do it at all! Overall though I just enjoy the simplicity of the syntax and I feel like it helps me think about the functions I write more.

Currently I have a gripe with the syntax. Its hard to know the return values of things like test_var, err := db.Exec(new_row) this is where I feel like C# spoiled me a bit, however we can just figure out the type by using fmt.Printf("%T", test_var). In the actual code test_var was just an underscore. I didn't know what this meant and it was clearly IMPORTant since one of the imports is named with an underscore. It is called the blank identifier, its a write only variable that stores values that don't get used so you don't have to create a whole new variable for output you're not planning to use, also saves you from avoiding the compiler screaming at you for never using an import during development.

In the program I want to be able to export a recipe from a JSON file that follows the simple protocol I made.

type Recipe struct {
    Name        string
    Ingredients []string
    Steps       string
}

Not a hard process but in Go it was a bit tricky to understand how it internally handles Hash Maps or in other languages like Python dictionaries Go simply calls them maps. To create a simple map I can just use the following line

data := make(map[string]int)
data["Name"] = "Aglio e olio" //name of a pasta recipe

I was thrown off by how it wants you to declare the types for the map, thats not something I am used to and my initial thought was along the lines of "what if its not the same throughout the file?" I mean bust open the settings JSON file for VSCode theres strings, numbers, arrays, and nested objects, how am I supposed to handle that?? Then I saw that you can assign the value to an interface{} which eased my thoughts, and quickly after that I was able to put two and two together that when you use json.Marshal() you are serializing data into JSON and vice versa with json.Unmarshal() and that you can use a struct or interface as the input to Unmarshal.

data, err := os.ReadFile("test.json")
if err != nil {
    fmt.Printf("Could not Unmarshal JSON: %s\n", err)
    return
}

rec := Recipe{}

error := json.Unmarshal(data, &rec) // would not let me name error return value "err"
if error != nil {
    fmt.Printf("Could not Unmarshal JSON: %s\n", err)
    return
}

fmt.Printf("JSON data: %s\n", rec)
fmt.Printf("%s\n", rec.Ingredients)

The last thing I want to do in this post is get the JSON into the DB and view it in the DB.

func InsertDb(db *sql.DB, data Recipe) { //accept JSON struct instead

    _, err := db.Exec(`
    INSERT INTO recipes (name, steps, ingredients)
    VALUES (
        $1, $2, $3 
    );`, data.Name, data.Steps, pq.Array(data.Ingredients))

    if err != nil {
        fmt.Println(err)
    }
}

Two things to note that I enjoy. The first is the syntax of fmt.Sprintf(). I just enjoy that its the same as fmt.Printf or if anyone has used C its just storing the output of printf in a variable. Really straight forward and simple to use. The other is how easy structs are to work with. if you don't need to change the values of the struct you pass just pass as value, if you need to alter the original pass as reference using &. These are things that are simple but to some that just use scripting languages or even languages like C# and Java. It feels lightweight and I thoroughly am enjoying it.

LINE 1: SELECT * FROM recipes;
                      ^
recipedump=# SELECT * FROM recipes;
 id |   name    |     ingredients     |   steps    
----+-----------+---------------------+------------
  1 | Test Name | {test1,test2,test3} | Test Steps
(1 row)

I just threw dummy data in to make sure everything was inserted correctly. I was arguing with the insert function a bit. I didn't realize that db.Exec formats the string for you in its own way, using $1 instead of just formatting the string with fmt.Sprintf() before hand, but other than that I didn't have any issues.

This is where I'm going to end this. So far I am really enjoying Go. There are some hiccups like if I save my file but I inserted a few new lines before hand it'll delete the lines. The same goes for if you import something but haven't came around to using it yet. Another instance of something like that is the compiler freaking out if you put else on its own line, that one is something that I think is unnecessary. Those nitpicks aside I really enjoy the language, and you will see it more in future posts.