This is my first Golang project, I was wondering if I could get some help to ensure I'm following best practices.
Here is my repo: https://github.com/Hypnophobe/go-cash
@ashen nexus
73 messages · Page 1 of 1 (latest)
This is my first Golang project, I was wondering if I could get some help to ensure I'm following best practices.
Here is my repo: https://github.com/Hypnophobe/go-cash
@ashen nexus
First off, I'd probably say have one entry point that uses a flag for the database location, with an optional flag to overwrite if it exists already
I don't see the value in having a separate binary to initialise the database
have a look through your code and see if you can find similar instances like this https://github.com/Hypnophobe/go-cash/blob/a215b28bc710d5cf81b4321ac51304b0d6fcb633/handlers.go#L46 where you could return from the if block and eliminate the need for an else block
You're using a global for the database interaction, which in a small project isn't neccearily the be all and end all of bad practices, but it is definitely better to have a struct with methods to better control the state and interaction with it
if this is an introductory project, it would be better to get a handle on best practices and avoid using a global
Hmm okay awesome, thank you for your input @ashen nexus
Is there any negative to using map, as opposed to using structs for everything?
From what I understand, structs are more performant?
they're very different concepts
A map is a key/value lookup, a struct is type that can contain fields and methods
If you need to store things that you need to reference later by a known key, then you could use a map
Ahhh okay
Would something like this be a decent use for a map:
response := map[string]interface{}{"ok": false, "error": "failed to retrieve addresses"}
any time you use interface{} or its alias any you're essentially bypassing the type system
so you'd usually have to ask yourself, is it absolutely necessary that I forego type safety for this
is there context for that snippet above?
ahhh right, thank you I didn't realize it bypassed the type system
by using interface{} your essentially saying "I have no idea what the type of this thing will be"
Sorry, I'm very tired and at work haha, I was wondering if that is a good use case for map and interface, but as you mentioned I think interface{} might be redundant, because I know the type is going to stay the same
I'm coming from Lua and I'm used to just using tables for everything, but I'm not sure if there's a better way to pass through {"ok":false}
Sorry if my terminology is poor, I'm not a professional developer, just a hobbyist
I would say for stuff like this https://github.com/Hypnophobe/go-cash/blob/a215b28bc710d5cf81b4321ac51304b0d6fcb633/wallet/main.go#L33 if it's outside of the reach of the flags package, then maybe look at urfave/cli
It'll help define the arguments and flags programmatically, and also generate help etc
What do you mean by outside of the reach of the flags package?
I'd avoid using functions like handleError https://github.com/Hypnophobe/go-cash/blob/a215b28bc710d5cf81b4321ac51304b0d6fcb633/wallet/main.go#L121 and just do the if err checks in place
the flags package is great for simple command line flags, but does lack some features, it's not great for repeated flags adding to a slice of the value for example, or for positional arguments and subcommands
Okay noted, will do
with urfave/cli it becomes quite easy to do commands and subcommands with their own set of flags and switches, and it'll nicely provide --help for each of them
Is there another way to do it better, besides using urfave/cli?
I'm trying to avoid using external modules where possible, and forcing myself to use the base modules/packages
Ah right disregard, you mentioned flags already
Switch case would be the way to go really, but it can be cleaned a bit. Simply inverting the condition at the start and for balance would read better
func main() {
if len(os.Args) < 2 {
printHelp()
}
switch os.Args[1] {
case "balance":
if len(os.Args) != 3 {
printHelp()
}
getBalance()
case "send":
sendTransaction()
default:
printHelp()
}
}
You should try and keep indentations and nesting as minimal as possible. If there is a way to invert the logic of a condition and return early instead of having to nest or use an else block then it's usually a good idea to do it
In those cases above, you would probably want to include an os.Exit(1) in printHelp() or after the calls to printHelp() as it's unlikely you'd want to print help and continue exution.
Awesome, thank you so much for taking the time to help!
For cases like
func getBalance() {
resp, err := http.Get(syncNode + "address/" + os.Args[2])
...
it would be much better to pass the required values as function parameters
generally speaking, the sooner you can capture external input (whether its config from files, environment variables, command line arguments) and convert them into values represented in your code the better
so it would likely end up something like
case "balance":
if len(os.Args) != 3 {
printHelp()
}
address := os.Args[2]
//maybe validate address
getBalance(address)
[...]
When you're looking objects in your code that represent something significant in your domain, like this:
transaction := map[string]interface{}{
"pkey": pkey,
"address": address,
"amount": amount,
}
It might be worth creating a struct for it, especially if it's used in multiple places throughout. It's likely this would be something in a specific package that this main.go could import. You could have something like
type Transaction struct {
PrivateKey []byte `json:"pkey"` //? not sure the type of "pkey" from above
Address string `json:"address"`
Amount int `json:"amount"`
}
func (t Transaction) Validate() error {
// logic to validate the transaction fields, is the address correct, is the Amount a positive integer etc
}
It's purely readability
the more you indent, the more context you have to keep in your mind as you're reading the code
Okay cool, that makes sense
I find it really hard to read other people's code, even if I know the language, so that seems logical
Awesome, that was my main issue with go so far. When to use structs
I've found Go tends to be relatively easy to read, especially as certain style choices are generalyl enforced by gofmt
I tend to use structs all the time, the first thing I do when approaching a problem is think about what the data looks like and how it can be defined so I can work with it
The next step is, how do I want to interact with this or get it to do stuff. Should it be responsible for its functionality, or is it part of something slightly bigger that should accept these things as function parameters to work on
So with your first step, would you typically also keep in mind the end result too?
Then as you said secondly, it's just a matter of interacting with that to get the desired result
I was also wondering if Go has some sort of equivalent to Lua's metatables?
how do you mean end result?
Having a quick look at Lua metatables, there isn't a direct equivalent in Go. The approach in Go would be pretty situational as to what you're trying to achieve
As in, trying to visualize the output of whatever you're doing with the data? Eg. Setting up structs for a response
you don’t need to add new dependencies like ufav for flags (nor cobra)
I always feel like I mess this kind of thing up but it's so dang important but sometimes I gget confused !
I thought about it and checked my bash history, and I was being silly. I hadn't called anything from the sqlite3 driver I go get'd, ran go tidy, and it must have removed it and I was not realizing it was cached
np. I usually put the import and then go mod tidy (skipping go get)
i just wanted to confirm, is the reasoning behind this for readability as well?
yes leftmost is best, avoiding unecessary else {} also
Okay awesome
@ashen nexus I have made some changes to my code (I reverted my github history because it was a mess, and im trying to follow better commits)
im about to work on using databases for structs and getting rid of interface so it's type safe now - by methods of the struct to control the database, would you just do something like this?
type DB struct {
conn *sql.DB
}
func (db *DB) Create(name string, age int) error {
query := "INSERT INTO users (name, age) VALUES ($1, $2)"
_, err := db.conn.Exec(query, name, age)
if err != nil {
return fmt.Errorf("failed to insert record: %w", err)
}
return nil
}
you installation instructions
git clone https://github.com/Hypnophobe/go-cash && cd go-cash && go build .
can be changed to much simpler/direct:
go install github.com/hypnophobe/go-cash@latest
ah okay cool
and add /wallet/ for the other binary I guess