#How am I supposed to go about this?

22 messages · Page 1 of 1 (latest)

sick trellis
#

Hi!
My name is Ember. I'm a developer, coming from Python, Java, HTML/CSS, etc. Mostly languages where you can make classes and thus construct objects, ie. easily exercise an object oriented approach. I was also trained somewhat extensively in that approach.

So, when I begin a project, I often begin by thinking of "parts" to create related objects. In essence, if I want to model a physical, analog clock, I start by making gears... depending on the degree of detail.

With Go, I'm building a bot, because why not, but then I ran into the fact that you can't make classes in Go! o.o

So, I could program this like I used to program, before I ever took a class, and just... put it all on one page, whatever "it" is (I do have something working).

But! I'd much rather go about this in the most orderly way possible! In Python, I'd have another file. What do I do in Go?

sacred vault
#

in go, you also can split your programs into different files

#

golang is like C, it doesn't have class or such inherit stuffs, you can only use struct to simulate OOP in other languages.

sacred vault
#

if you like make submodule, just create a subfolder, and use import "path/to/your/project/subfolder" to import your sub module/package

#

path/to/your/project is actually the path recorded in your go.mod file, not the actual path to your project

#

This would be an usual go.mod file

module github.com/xxx/xxx

go 1.20
frozen kayak
#

You can associate data with functions – i.e. have methods on objects. Define a struct and methods like so:

type Bot struct {
  // some fields
}

func (b *Bot) DoStuff() {
  // use the fields: b.<fieldName>
}

You can use the bot like so:

var b Bot
b.DoStuff()
#

This method syntax is really a kind of sugar for what would be:

func Bot_DoStuff(b *Bot)
#

In fact, the following code is valid and proves the point:

type Bot struct{}

func (b *Bot) DoStuff() {}

func main() {
    fn := (*Bot).DoStuff
    fn(&Bot{})
}
#

The only special thing about structs with methods is that they can implement interfaces. An interface looks like this:

type Stuffer interface {
  DoStuff()
}

Interfaces are implemented automatically – no need for implements. So you can have a function like the following:

func UseStuffer(s Stuffer) {
  s.DoStuff()
}

which you can call like:

UseStuffer(&Bot{})

It is a quintessential detail, that interfaces are implemented automatically. This dictates how interfaces are used: consumers define interfaces, not implementors, as in other languages. For example, if you have a HTTP service that expects a database, the interface:

type Database interface {
  GetData() (Data, error)
}

will be defined by the HTTP package, not by the database package and then used by the HTTP one.

#

Now, onto packages. As said above, you can have multiple files in Go and multiple folders. A folder is a package, and the name of the folder is the name of the package. There will be a root package, usually where your repository is initialized, and this root folder will contain the aforementioned go.mod file.

#

In the following file structure:

|-data
| |-data.go
|-database
| |-postgres.go
|-server
| |-server.go
|-main.go
|-go.mod

with the following go.mod contents:

module github.com/UnpickableWhiplash/bot

go 1.20

here's how the code will look like in each file:
data/data.go:

package data

type Data struct {
  Name string
  // others...
}

database/postgres.go:

package database

import "github.com/UnpickableWhiplash/bot/data"

type Postgres struct {
  // contains some fields
}

// GetData implements the server.Database interface.
func (p *Postgres) GetData() (data.Data, error) { /* impl */ }

server/server.go:

package server

import (
  "net/http"
  "github.com/UnpickableWhiplash/bot/data"
)

type Server struct { /* contains fields */ }

// ServeHTTP implements the http.Handler interface, so it can be used
// to serve HTTP requests using the standard library's
// HTTP implementation.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { /* impl */ }

type Database interface {
  GetData() (data.Data, error)
}

// New creates a server. Notice that it's not named NewServer,
// because at call site the function will be called as server.New().
func New(db Database) *Server { /* impl */ }

main.go

package main

import (
  "net/http"
  "github.com/UnpickableWhiplash/bot/database"
  "github.com/UnpickableWhiplash/bot/server"
)

func main() {
  // because *server.Server has the ServeHTTP method, we can use it
  // as an HTTP handler (it implements the http.Handler interface).
  // because *database.Postgres has a GetData method, we can use it
  // as a server.Database to initialize the server.
  http.Handle("/", server.New(&database.Postgres{}))
  http.ListenAndServe(":8080", nil)
}
#

This paradigm of "consumer defines the contract/interface" enables us to have infinitely many implementations and to easily swap them – nothing couples the consumer to the implementor, as in other languages. This is the crux of programming in Go, and what will make or break your code.

#

As for modules, what in Python would be a file, here in Go it is a folder. Folders, not files, are modules. All files inside a folder are part of the same module and without importing they can access symbols from other files. For example, files module/a.go and module/b.go automatically share with the other any type or function defined in each of them, without having to import anything or declare the symbols as exported (with capital letter at the beginning).

#

Keep in mind that capital letter at the beginning of type/function/method/constant/variable name is not mere convention – it declares the symbol as exported and makes it visible from outside the package.

#

As a last note, make sure your exported names are not redundant. Given that to use exported symbols from a package you have to also write the package name, a name like NewServer for a function inside the server package is redundant – server.NewServer??? Keep names concise – and naming packages suggestively will help shortening the symbol names. Also, not having util packages helps – notice the difference between util.SplitString and strings.Split (the latter package is a stdlib one). Never have catch-all packages like misc, util, and avoid as much as you can packages like model, middleware, but group by domain instead: have auth.Middleware and auth.Service, not services.Auth and middleware.Auth, for example.

#

In short, you can nicely create your "parts", group related data and functionality, create modules, abstractions and so on. The approach differs in the sense that it is not "top-down" – inheritance, explicit implements – but rather "bottom-up", I would say – composition, implicit implements. In Go, the consumer controls everything: a bigger struct chooses what it is composed of, what features it takes from the smaller structs it is composed of, and consumers define interfaces, the contracts which should be implemented. It's a rather capitalist approach – demand is more important than supply – whereas in classic OOP, as in communism, supply triumphs. Just as in politics and society, the programming world veers nowadays towards the former.

sick trellis
#

Ha ha!! This community is amazing; thank you so much for all of this detail! 😄

sick trellis
#

and having read, really, one of the best responses i've gotten to most anything.