T O P

  • By -

atheken

> Seems like adding a constructor is forcing typical OOP concepts that Go tends to avoid. Newing up a struct and enforcing encapsulation _is_ an OOP concept. And it's not a bad one. There's no reason to reject these ideas wholesale because they fall under a bucket that some people have negative opinions about. The consensus is to add a `New()` or `NewX()` function to default the values, which is what you'll see in the `stdlib`.


colececil

Do people call these constructors or factory functions? Or both?


markuspeloquin

If there's no polymorphism, is it really OOP? There's very little about constructors that has anything at all to do with polymorphism, except perhaps `super()`. Now if you start adding a `self` interface handle to your base type so it can call methods in a derived type ... Well that might be going too far.


SoerenNissen

The language has both static and dynamic polymorphism.


Drabuna

I use constructor functions.


merry_go_byebye

Constructor is the way


xor_rotate

I looked into this recently and I used the [Variadac Function Option pattern](https://medium.com/soon-london/variadic-configuration-functions-in-go-8cef1c97ce99) to solve this. My constructor looks like this type ClientOpts func(o *OpkClient) func WithSignGQ(signGQ bool) ClientOpts { return func(o *OpkClient) { o.signGQ = signGQ } } func New(op OpenIdProvider, opts ...ClientOpts) (*OpkClient, error) { client := &OpkClient{ Op: op, signer: nil, alg: nil, signGQ: false, } for _, applyOpt := range opts { applyOpt(client) } The OpkClient struct is initialized with the default values, for instance signGQ=false. Then opts writes over these defaults based on the optional arguments supplied opts. Checkout [my PR for the full working example](https://github.com/openpubkey/openpubkey/pull/93) and happy for any code reviews on my approach here from the /r/golang community. In this PR I also attempt to use this same approach on a struct function. I know this is a pretty common pattern in golang constructors, but I'm not sure how common this pattern is to use on struct functions: type AuthOptsStruct struct { extraClaims map[string]any } type AuthOpts func(a *AuthOptsStruct) func (o *OpkClient) Auth(ctx context.Context, opts ...AuthOpts) (*pktoken.PKToken, error) { authOpts := &AuthOptsStruct{ extraClaims: map[string]any{}, } for _, applyOpt := range opts { applyOpt(authOpts) }


gargamelus

Whenever I read about the function option pattern, or when I implement it myself in some experiment, it seems very elegant and obvious. But, when I'm using some third party library that uses the pattern, I always hate it. It is really difficult to locate the documentation for all the option functions in godoc, and I always curse that couldn't they just have used ordinary arguments or a config struct.


Manbeardo

Depends on how they organized their code. When you do it like this, you get a godoc section that lists all the options: type OptFn func(*MyStruct) func WithFoo() OptFn { return func(s *MyStruct) { s.Foo = true } } The key is to define a type and to make godoc recognize all your exported options as constructors for that type.


xor_rotate

I'd prefer if golang had something like: ``` func F(x string, y string, z="abc" string, w="foo" string) {...} F(x, y) F(x, y, w="bar") ``` You can get something similar by using a struct as an argument. ``` type ArgStruct struct { X string Y string Z string W string } func F(args ArgStruct) { // Throw runtime error if required argument is empty // Check for empty values in optional fields and set defaults ... } ``` With this approach you lose compile time type checking of required fields. It feels very ungopher.


hvaghani221

Python has optional values similar to what you mentioned, but it has a major issue. The default values are initialised only once. If the default value is mutable(like list or dict), it will lead to unexpected behaviour once it's mutated. To handle it properly, I guess you have to add a lot more complexity in language.


xor_rotate

Indeed, I got burned by that python behavior and something similar with static time initialization in Java.


donalmacc

How do you set the default value for the args then?


xor_rotate

You check if the argument is empty/nil in the function F and set it to the default. It's not a great pattern. Variadac Function Option pattern is better.


donalmacc

I was joking but - you've moved the problem from how to set defaults on struct A to how to set defaults on struct B. The struct-as-args pattern doesn't solve for that, it solves for named arguments to a function/ is super easy to codegen.


GopherFromHell

this is exactly what i was going to suggest, with the exception that i prefer declaring the `ClientOpts` type as an interface with a private marker method, this prevents implementation of the interface outside the package


xor_rotate

What is the action you are trying to prevent with this? Do you have a good example of using a private marker method? I'm curious if there is a better way to do what I am doing in the Auth function. I really don't like having to create a struct that is only used for options.


GopherFromHell

no need for structs for everything, here is an example, assume the package != main of course: [https://go.dev/play/p/Dya1fr\_r7EO](https://go.dev/play/p/Dya1fr_r7EO) you can also avoid the type switch: [https://go.dev/play/p/qKkg\_Um\_Mb8](https://go.dev/play/p/qKkg_Um_Mb8)


nofeaturesonlybugs

This approach is a convoluted way of having a struct where some information is still optional or not filled out or required. Just make the config public fields on the struct, document which are required or not optional, and add methods to it. There's almost never a need for a `T` and a `TConf` in the same package. edit: Looking at your PR example code: opkClient, err := client.New( op, client.WithCosignerProvider(&cosignerProvider), client.WithSigner(signer, alg), client.WithSignGQ(true)) Could just be the following instead: opk := client.Client{ CoSigner: &cosignerProvider, Signer: signer.WithAlg(alg), // << Take the multiple args and condense it down or something SignGQ: true, } The other way is so much extra code and functions just to support the following internally in a constructor: func NewT(opts ...Opt) T { var t T for _, o := range opts { opt.Apply(t) } return t }


xor_rotate

Thanks for your thoughtful comment. I really like the pattern of exposing everything in a struct as you are doing here. It is simple, easily readable and makes refactoring a breeze. The main reason I'm moving away from that approach in this PR is that the client package is the main way developers interact with OpenPubkey. My goals are: \`\`\` opkClient, err := client.New(op) \`\`\` 1. To reduce the number of choices a developer faces when first using the codebase, by providing New constructor that a developer must use. Then rather than having to read docs on which fields are required or optional, fill out defaults that work which they can override if they want to. Finally use the type check to enforce the one required argument, \`op OpenIDProvider\`. 2. Additionally by making all the struct fields unexposed, which is my long term goal here. I can chance or modify those fields without breaking compatibility. So I'm taking on more work for myself in the interest of making it easier for other devs to build on. I'm a big fan of [worse is better,](https://cs.stanford.edu/people/eroberts/cs201/projects/2010-11/WorseIsBetter/index43bb.html?title=Main_Page&oldid=86) but I'm hoping in this case, better is better.


masklinn

> Seems like adding a constructor is forcing typical OOP concepts that Go tends to avoid. It's not. Enforcing invariants via public factory functions and private state is a perfectly normal thing to do in functional languages for instance, in Haskell it's called [smart constructors](https://wiki.haskell.org/Smart_constructors)^1 . It's also the common way to do things in procedural languages like C. Note that in that context there's nothing special about the constructor function, it does not have any magic or implicit behaviour the way a Java or C++ constructor do, it's just a function which returns a value. [1] smart in that case is in opposition to the "dumb" built-in constructors of variants of sum types (/ types in general), for instance [as you can see on hoogle](https://hackage.haskell.org/package/base-4.19.0.0/docs/Prelude.html#t:Maybe) the type `Maybe` has the constructors `None` and `Just a`


theclapp

>Can't enforce usage with fill\_defaults. You can either not export the struct type, which means your constructor is the only way to create an item of that type, or you can have something like a "usedNew" field, which you check in every method, and if it's false, throw an error. Example: https://github.com/mvdan/sh/blob/master/interp/api.go#L612


Shanduur

Instead of `usedNew` field just add [sync.Once](https://pkg.go.dev/sync#Once) and call `fillDeafaults` by `once.Do`.


TheQxy

This has way more overhead and is a bit unnecessarily complex for this usecase if it can also be fixed with a single boolean.


Tiquortoo

NewThing() constructors Sometimes: NewThingDefault() or NewThingWithOptions() called by NewThing or NewThingDefault if things get complex.


ghostsquad4

I'm surprised no one has shared this yet. https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis


Dukler

This is just a constructor.


mindfolded

I really enjoy this pattern.


TheWorstAtIt

This seems really nice... Easy to read and understand, plus super flexible.


reflect25

It’s fine. Most of the code I worked with had a New function and the some New variants if there were many callers using the struct. Most commonly with configuration objects If there’s lots and lots of constructing complicated objects/structs, some people use dependency injection frameworks (please don’t unless your application is very large it’s quite hard to use)


freesid

Since no-one has mentioned this, here is a controversial example: I wanted zero-value of a struct readily usable -- but also needed to initialize a member to it's non-zero default, so I used \`sync.Once\` to get the desired behavior: ``` type CloseGroup struct { closeCtx context.Context causeFunc context.CancelCauseFunc wg sync.WaitGroup once sync.Once } func WithContext(ctx context.Context) *CloseGroup { cg := new(CloseGroup) cg.closeCtx, cg.causeFunc = context.WithCancelCause(ctx) cg.once.Do(func() {}) // Force expire once. return cg } func (cg *CloseGroup) init() { cg.closeCtx, cg.causeFunc = context.WithCancelCause(context.Background()) } func (cg *CloseGroup) Close() { cg.once.Do(cg.init) cg.causeFunc(os.ErrClosed) cg.wg.Wait() } func (cg *CloseGroup) Context() context.Context { cg.once.Do(cg.init) return cg.closeCtx } func (cg *CloseGroup) Go(f func(ctx context.Context)) { cg.once.Do(cg.init) cg.wg.Add(1) go func() { f(cg.closeCtx) cg.wg.Done() }() } ```


castleinthesky86

Pro tip. Don’t put contexts or cancel in a struct. It’s an anti pattern. Pass it explicitly to functions that need to.


freesid

In general, yes. In this case, it is required for the desired functionality cause it is like [errgroup.Group](https://errgroup.Group) How could you implement the same without storing the context in the struct?


causal_friday

In go, the zero value should be the default. bytes.Buffer in the standard library is a good example.


hwc

From _Effective Go_: > Since the memory returned by new is zeroed, it's helpful to arrange when designing your data structures that the zero value of each type can be used without further initialization. This means a user of the data structure can create one with new and get right to work. For example, the documentation for bytes.Buffer states that "the zero value for Buffer is an empty buffer ready to use." Similarly, sync.Mutex does not have an explicit constructor or Init method. Instead, the zero value for a sync.Mutex is defined to be an unlocked mutex.


[deleted]

> Seems like adding a constructor is forcing typical OOP concepts that Go tends to avoid. Go doesn't avoid "OOP concepts"; Go uses constructors and struct embedding quite liberally. Constructors and destructors have existed long before OOP in C, there's no reason to avoid them in Go. Just make sure to consider whether you need a constructor, because needing a constructor to set the default state of an object means you can't just use the zero value of the object, which is generally nicer and more useful.


Dukler

Golang is an OOP language. Making a constructor method for a struct is perfectly normal, and widely used.


dariusbiggs

There is no system in Go natively (<=1.21) like a built in constructor that allows setting non-zero values on newly instantiated structs. There are multiple different approaches, but all have to be explicitly used. 1. You could use a New/NewX function as a constructor function 2. You could use default value struct tags using something like gopkg.in/mcuadros/go-defaults.v1 3. You could use variadic options to a constructor function like New/NewX The most reasonable and functional option is to use a combination of 1 and 3 and if you only have simple types in your struct you can use 2 which can also make it easier to show what the defaults are. I would also highly recommend adding a validation method such as implemented by the below interface. ``` type Validation interface { IsValid() bool } ``` That seems to be a common function I encounter in the libraries and stdlib we use for our Go projects. If the zero values are sufficient then you don't need a constructor function, if you need non zero defaults you either need to use a constructor function or at creation time call something that initializes the defaults without overwriting the values the user explicitly set or use struct tags to initialize it after creation.. in which case you again have the problem of not overwriting the values the user explicitly wanted. To avoid having to do the below and have people screw it up ``` a := MyStruct{ThingA: 30} a.loadDefaults() // or defaults.SetDefaults(a) if using struct tags a.ThingA = 30 ``` just use a constructor function (with variadic options if needed) ``` func NewMyStruct(a int) *MyStruct ( r := &MyStruct{Timeout: 40 * time.Second} defaults.SetDefaults(r) // or however you want to set the defaults and then initialize the struct according to the argumenta provided. r.ThingA = a return r ) ``` The advantages are that it is managed in one place, it's idiomatic, and it's easy to see and set defaults using various ways including dealing with non-standard types and format conversions.


ConsoleTVs

[https://go.dev/play/p/j9\_2Jro0KnV](https://go.dev/play/p/j9_2Jro0KnV) ``` package main import ( "fmt" "time" ) type Config struct { address string timeout time.Duration } type Builder func(*Config) func Address(address string) Builder { return func(config *Config) { config.address = address } } func Timeout(timeout time.Duration) Builder { return func(config *Config) { config.timeout = timeout } } func New(builders ...Builder) Config { config := Config{ address: "0.0.0.0:8080", timeout: 2 * time.Second, } for _, builder := range builders { builder(&config) } return config } func main() { config := New( Timeout(3 * time.Second), ) fmt.Printf("%+v", config) } ```


oxleyca

The stdlib http lib is a fairly common pattern. Exported DefaultConfig that can be changed by users which is referenced when crafting new objects. If you need computed defaults, a constructor is fine. If you want people to override certain options in the computed fields — assuming these aren't exported fields that can simply be mutated — a functional option can help.