optional

package module
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 3, 2025 License: MIT Imports: 5 Imported by: 6

README

optional

Go library for working with optional values

How to use

Importing
import (
	"github.com/brnsampson/optional"      // For primative types
	"github.com/brnsampson/optional/file" // For files and certificates
)
Setting optional values
	// There are types for all the primatives we would normally expect
	// Bool
	// Int Int16 Int32 Int64
	// Uint Uint16 Uint32 Uint64
	// Float32 Float64
	// Str
	// Time
	// and the generic Option[T comparable]

	// Create an Optional Int with no initial value
	// The zero value of an optional is None
	var i optional.Int

	// This also works fine
	i = optional.Int{}

	// However, I normally use this functional form for symmetry with creating
	// Options with values, optional.SomeInt(<my int>)
	i = optional.NoInt()

	// Check if i is None (empty)
	if i.IsNone() {
		fmt.Println("i is empty!")
	}

	// Set the value to some default if it was previously unset
	i.Default(42)

	// Update the value and get the previous value back for any comparisons you might need to do
	previous := i.Replace(42)

	// Some methods like Option.Replace() return an Optional interface type. This erases the
	// concrete type and hides all of the methods which could mutate the value,
	// as the previous value is only provided as a reference. Unfortunatly, this also
	// hides some convenient things like the implemntations of TextMarshaler and Stringer
	if previous.IsSome() {
		fmt.Println("Replaced previous value: ", previous.MustGet())
	}

	// Overwrite the previous value without care
	i = optional.SomeInt(42)

Inspecting the value of an Optional
	i := optional.SomeInt(42)

	// Check if i has a value
	if i.IsSome() {
		fmt.Println("i has a value!")
	}

	// We can check to make sure i is 42
	if i.Match(42) {
		fmt.Println("i was indeed 42!")
	} else {
		return errors.New("somehow failed to match something that really should have matched")
	}

	// Get i's value along with an 'ok' boolean telling us if the read is valid
	val, ok := i.Get()
	if ok {
		fmt.Println("Got i's value: ", val)
	}

	// Get i's value, but just panic if i is None
	val = i.MustGet()

	// Get i's value or a default value if i is None
	tmp := optional.GetOr(i, 123)
	fmt.Println("Got i's value or 123: ", tmp)

	// Get i's value or a default value AND set i to the default value if it is used
	// Note that helper functions require a MutableOptional interface, which only Option
	// Pointers fulful. That should be a given, since it's just like passing an int;
	// you can't expect a function to modify an int, it can only return a new int.
	tmp, err := optional.GetOrInsert(&i, 42)
	if err != nil {
		fmt.Println("Error while replacing i's value with default")
	} else {
		fmt.Println("Got i's value which should DEFINITELY be 42: ", tmp)
	}

	// For functions that automatically convert types into their string representation,
	// the Option can be used directly:
	fmt.Println("Printing i directly: ", i)
Marshaling values
	i := optional.SomeInt(42)
	f := optional.SomeFloat32(12.34)
	s := optional.SomeStr("Hello!")
	nope := optional.NoStr()
	secret := optional.SomeSecret("you should only see this if it is marshaled for the wire!")

	// Let's create a text string first using Sprintf. We can't use more specific verbs like
	// %d or %f because we have no way to represent None. Note that our Secret will be redacted.
	newString := fmt.Sprintf("i: %s, f: %s, s: %s, nothing: %s, secret: %s", i, f, s, nope, secret)
	fmt.Println("Created a new string from optionals: ", newString)

	// Options do have TextMarshaler and String methods implemented though, so we can equally well use %v
	newString = fmt.Sprintf("i: %v, f: %v, s: %v, nothing: %v, secret: %v", i, f, s, nope, secret)
	fmt.Println("Created another new string from optionals: ", newString)

	// Now let's marshal a json string
	type MyStruct struct {
		Int          optional.Int
		Float        optional.Float32
		GoodString   optional.Str
		BadString    optional.Str
		SecretString optional.Secret
	}

	myStruct := MyStruct{i, f, s, nope, secret}
	jsonEncoded, err := json.Marshal(myStruct)
	if err != nil {
		fmt.Println("Failed to marshal json from struct!")
		return err
	}

  // We DO expect our secret to be printed here, as we have explicitly marshaled it into
  // a byte array.
	fmt.Println("Json marshaled struct: ", string(jsonEncoded))
Transforming Values
	// Define our value and transformation first
	i := optional.SomeInt(42)
	transform := func(x int) (int, error) { return x + 5, nil }

	// Modify the value in an Option without unpacking it
	err := i.Transform(transform)
	if err != nil {
		fmt.Println("The transform function returned an error!")
		return err
	}

	// Apply our transform to a slice of options, while modifying None values to be their index in the slice.
	// Remember, the zero value is None
	opts := make([]optional.Int, 10)
	for i, opt := range opts {
		// Functions which modify options in place should accept the MutableOptional interface which
		// is implemented by Option pointer types, such as this helper function. Try to use optional.TransformOr
		// with opt instead of &opt. It doesn't work, just in the same way that passing an int into a function
		// and expecting the integer to be changed in place doesn't work.
		err = optional.TransformOr(&opt, transform, i)
		if err != nil {
			fmt.Println("The transform function returned an error!")
			return err
		}
	}

How to use the file package

Loading and reading from files
	// However we got it, we either have or do not have a path. For our example, let's assume we loaded this from a
	// flag so we end up with a *string which could be nil

	f := file.NoFile()
	if path != nil {
		f = file.SomeFile(*path)
	}

	// Just read the contents of a file. Acts like os.ReadFile(path), but returns an optional Str
	// containing the contents as a string if the file had any contents, // or a None if it was empty.
	// If there was some kind of error (e.g. the file does not exist or is not readable), then the
  // second ok value will be false. This is set up this was so that you can just call
  // contents := f.ReadFile() and use the value without really thinking about it.
	contents, ok := f.ReadFile()
	if !ok {
		fmt.Println("Failed to read from file")
	} else {
		fmt.Println("Got file contents: ", contents)
	}

	// Open a file for reading. File.Open() works just like os.Open(path),
	// so the file is opend in ReadOnly mode.
	var opened *os.File
	opened, err = f.Open()
	if err != nil {
		fmt.Println("Failed to open file for reading: ", err)
		return err
	}
	defer opened.Close()

	// Now use the file handle exactly as you would if you called os.Open()
	return nil
Loading secrets from files
	// There is a SecretFile type for convenience since this is a common thing to do
	// in an application. SecretFile simple overrides a few methods of File so that
	// we get a Secret option out of loading the contents instead of a Str.
	f := file.NoSecretFile()
	if path != nil {
		f = file.SomeSecretFile(*path)
	}

	// You can also upgrade a File to a SecretFile
	var normf file.File
	if path != nil {
		normf = file.SomeFile(*path)
	}
	secretf := file.MakeSecret(&normf)

	// You can still see the filepath and everything for a secret file, but we
	// do assume some things about secret files such as the premissions allowed.
	valid, err := secretf.FilePermsValid()
	if err != nil {
		fmt.Println("Failed when validating file permissions for secretf!")
		return err
	}

	if !valid {
		fmt.Println("File permissions for a SecretFile were not 0600!")
	}

	// normf will be cleared as part of upgrading a File to SecretFile
	if normf.IsNone() {
		fmt.Println("Sucessfully cleared normal file after upgrading it to a secret file.")
	}

	// Calling ReadFile() on a SecretFile produces a Secret
	secret, ok := f.ReadFile()
	if !ok {
		fmt.Println("Failed to read from file")
	}

	// This is a secret, so we will only see a redacted value when we try to write it
	// to the console. The same will happen if we try to log it.
	fmt.Println("Got secret file contents: ", secret)

	// Similarly if we try to use stdlib logging libraries
	slog.Info("Second try printing secret file contents", "secret", secret)
	log.Printf("Third try printing secret file contents: %s", secret)
Writing and deleting files
	f := file.NoFile()
	if path != nil {
		f = file.SomeFile(*path)
	}

	// Delete a file. Works like os.Remove, but also returns an error if the path is still None
	err := f.Remove()
	if err != nil {
		fmt.Println("Failed to remove file: ", err)
	}

	// Write the contents of a file. Acts like os.WriteFile(path)
	data := []byte("Hello, World!")
	err = f.WriteFile(data, 0644)
	if err != nil {
		fmt.Println("Failed to write file: ", err)
	}

	// Open a file for read/write. File.Create() works like like os.Create(path), which means
	// calling this will either create a file or truncate an existing file. If you want to
	// append to a file, you must use File.OpenFile(os.O_RDWR|os.O_CREATE, 0644) in the same way
	// that would need to when calling os.OpenFile. See https://pkg.go.dev/os#OpenFile for details.
	var opened *os.File
	opened, err = f.Create()
	if err != nil {
		fmt.Println("Failed to open/create file: ", err)
		return err
	}
	defer opened.Close()

	// Now use the file handle exactly as you would if you called os.Create(path)
	opened.Write(data)

	return nil
Other file tools
	f := file.NoFile()
	if path != nil {
		f = file.SomeFile(*path)
	}

	// Read back the path
	p, ok := f.Get()
	if ok {
		fmt.Println("Got path: ", p)
	} else {
		fmt.Println("No path given!")
		os.Exit(1)
	}

	// Check if the given path is the same as some other path, matching all equivalent absolute and relative paths.
	// In this case, check if the given path is equivalent to our working directory.
	if f.Match(".") {
		fmt.Println("We are operating on our working directory. Be careful!")
	} else {
		fmt.Println("We are not in our working directory. Go nuts!")
	}

	// Get a new optional with any relative path converted to absolute path (also ensuring it is a valid path)
	abs, err := f.Abs()
	if err != nil {
		fmt.Println("Could not convert path into absolute path. Is it a valid path?")
		return err
	}

	// Stat the file, or just check if it exists if you don't care about other file info
	if abs.Exists() {
		fmt.Println("The file exists!")
	}

	info, err := abs.Stat() // I don't care about the info
	if err != nil {
		fmt.Println("Could not stat the file")
	} else {
		fmt.Println("Got file info: ", info)
	}

	// Check that the file has permissions of at least 0444 (read), but is not 0111 (execute).
	// If those conditions are not fulfilled, we will set perms to 0644.
	valid, err := abs.FilePermsValid(0444, 0111)
	if err != nil {
		fmt.Println("Could not read file permissions!")
		return err
	}

	if !valid {
		err = abs.SetFilePerms(0644)
		if err != nil {
			fmt.Println("Failed to set file perms to 700")
			return err
		}
	}

How to load certificates and keys

Loading a TLS certificate
	// Similarly to the File type, the Cert and PrivateKey types make loading and using optional certificates
	// easier and more intuitive. They both embed the Pem struct, which handles the loading of Pem format files.

	// Create a Cert from a flag which requested the user to give the path to the certificate file.
	// Certs and Key Options also return an error if the path cannot be resolved to an
	// absolute path or the file permissions are not correct for a certificate or key file.
	certFile := file.NoCert()
	var err error
	if certPath != nil {
		certFile, err = file.SomeCert(*certPath)
		if err != nil {
			fmt.Println("Failed to initialize cert Option: ", err)
			return err
		}
	}

	// We can use all the same methods as the File type above, but it isn't necessary to go through all of the
	// steps individually. The Cert type knows to check that the path is set, the file exists, and that the file permissions
	// are correct as part of loading the certificates.
	//
	// certificates are returned as a []*x509.Certificate from the file now.
	// Incidentally, we could write new certs to the file with certfile.WriteCerts(certs)
	certs, err := certFile.ReadCerts()
	if err != nil {
		fmt.Println("Error while reading certificates from file: ", err)
		return err
	} else {
		fmt.Println("Found this many certs: ", len(certs))
	}

	// Now we want to load a tls certificate. We typically need two files for this, the certificate(s) and private keyfile.
	// Note: this specifically is for PEM format keys. There are other ways to store keys, but we have not yet implemented
	// support for those. We do support most types of PEM encoded keyfiles though.

	// Certs and Key Options also return an error if the path cannot be resolved to an
	// absolute path or the file permissions are not correct for a certificate or key file.
	var keyFile file.PrivateKey // Effectively the same as privKeyFile := file.NoPrivateKey()
	if keyPath != nil {
		keyFile, err = file.SomePrivateKey(*keyPath)
		if err != nil {
			fmt.Println("Failed to initialize private key Option: ", err)
			return err
		}
	}

	// Again, we could manually do all the validity checks but those are also run as part of loading the TLS certificate.
	// cert is of the type *tls.Certificate, not to be confused with *x509Certificate.
	cert, err := keyFile.ReadCert(certFile)
	if err != nil {
		fmt.Println("Error while generating TLS certificate from PEM format key/cert files: ", err)
		return err
	}

	fmt.Println("Full *tls.Certificate loaded")

	// Now we are ready to start up an TLS sever
	tlsConf := &tls.Config{
		Certificates:             []tls.Certificate{cert},
		MinVersion:               tls.VersionTLS13,
		CurvePreferences:         []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
		PreferServerCipherSuites: true,
		CipherSuites: []uint16{
			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
			tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_RSA_WITH_AES_256_CBC_SHA,
		},
	}

	httpServ := &http.Server{
		Addr:      "127.0.0.1:3000",
		TLSConfig: tlsConf,
	}

	// The parameters ListenAndServeTLS takes are the cert file and keyfile, which may lead you to ask, "why did we bother
	// with all of this then?" Essentially, we were able to do all of our validation and logic with our configuration
	// loading and can put our http server somewhere that makes more sense without just getting panics in our server code
	// when the user passes us an invalid path or something. We are also able to get more granular error messages than just
	// "the server is panicing for some reason."

	fmt.Println("Deferring https server halting for 1 second...")
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	go func() {
		<-ctx.Done()
		haltctx, haltcancel := context.WithTimeout(context.Background(), time.Second)
		defer haltcancel()
		if err := httpServ.Shutdown(haltctx); err != nil {
			fmt.Println("Error haling http server: ", err)
		}
	}()

	fmt.Println("Starting to listen on https...")
	if err = httpServ.ListenAndServeTLS("", ""); err != nil {
		// This kind of happens even when things go to plan sometimes, so we don't return an error here.
		fmt.Println("TLS server exited with error: ", err)
	}
Loading Private and Public keys
	// In some situations you want to use a public/private keypair for signing instead.
	// Here is how we would load those:
	var privFile file.PrivateKey // Effectively the same as privKeyFile := file.NoPrivateKey()
	var err error
	if privPath != nil {
		privFile, err = file.SomePrivateKey(*privPath)
		if err != nil {
			fmt.Println("Failed to initialize private key Option: ", err)
			return err
		}
	}

	var pubFile file.PubKey // Effectively the same as pubKeyFile := file.NoPubKey()
	if pubPath != nil {
		pubFile, err = file.SomePubKey(*pubPath)
		if err != nil {
			fmt.Println("Failed to initialize private key Option: ", err)
			return err
		}
	}

	// NOTE: as is usually the case with golang key loading, this returns pubKey as a []any and you have to kind of
	// just know how to handle it yourself.
	pubKeys, err := pubFile.ReadPublicKeys()
	if err != nil {
		fmt.Println("Error while reading public key(s) from file: ", err)
		return err
	} else {
		fmt.Println("Found this many public keys: ", len(pubKeys))
	}

	// While a public key file may have multiple public keys, private key files should only have a single key. This
	// key is also returned as an any type which you will then need to sort out how to use just like any other key
	// loading.
	privKey, err := privFile.ReadPrivateKey()
	if err != nil {
		fmt.Println("Error while reading private key from file: ", err)
		return err
	}

	fmt.Println("Loaded a private key from file")
	switch key := privKey.(type) {
	case *rsa.PrivateKey:
		fmt.Println("key is of type RSA:", key)
	case *dsa.PrivateKey:
		fmt.Println("key is of type DSA:", key)
	case *ecdsa.PrivateKey:
		fmt.Println("key is of type ECDSA:", key)
	case ed25519.PrivateKey:
		fmt.Println("key is of type Ed25519:", key)
	default:
		return errors.New("unknown type of private key")
	}

Secrets

There is a wrapper around optional strings named Secret which comes in handy when loading, well, secrets. You still need to use their values in code and marshal them into messages as appropriate, but ensuring keys and passwords do not end up in your logging system is always a big headache.

Using the Secret type is exactly the same as Str, but the methods used by string formatting and logging is overwritten to prevent secrets from being logged accidentally.

Some operations, such as loading content from a SecretFile will return a Secret instead of a Str to make things easier for you.

What?

Have you ever needed to represent "something or nothing"? It's common in go to use a pointer for this, but in some situations that can add complexity or is otherwise undesirable.

This package aims to provide an alternative, which is the "Optional". An Optional puts another type in a box which can either be "Some" or "None". That, and a small handful of methods to interact with the inner value, is pretty much it.

The goal of this isn't to be particularly performant. This ain't no rust style "zero-cost abstraction". Don't use it in the hot paths of your code - that's where the pointer trick goes.

The goal is to provide the functionality that works in an intuitive way without any of the gotchas and questions that using a pointer would bring so that you can implement what you need quickly without any thought. If you find performance is an issue, then you can always replace these with pointers later.

Really, this is a generic solution to a common problem: How do you represent NOT a thing? Often we can find a particular "magic value" in the domain of our type which is not a valid value for our application and use that. Have a field for email address? An empty string can be used as your None value. Port number? Pick anything above 65,535 to represent "None selected". Sometimes, however, there is no obvious invalid value of your type in your use case. Moreover, you sometimes need to differentiate between "default" and "None", and in those cases you actually need two values which are both invalid for your application!

Disclamer: I'm sure you could find exciting new ways to shoot yourself in the foot if you tried to create an optional of a pointer type (or a struct which contains pointer fields). You can definitely do it, I just don't advise doing so unless you know what you are doing.

Where?

Where Optionals really shine, are situations which are not performance sensitive in the first place. Configuration is probably the best example, particularly if you are drawing configs from multiple sources and merging them together. I actually first wrote this in a fit of irritation after tracking down a nil-pointer deference in a configuration package for longer than it took to write the package.

How?

See the examples/simple_config/ directory for a setup for brain-dead config parsing. Sure, it's verbose for two real parameters and there are is a lot of boilerplate for what it does, but nothing is going to go wrong and it is fully extendable to a real world project without needing any complicated additional libraries involved. Don't get me wrong, Cobra and Viper are super powerful and well maintained, but 98% of the time I only really want one command per executable and every time I touch Cobra or Viper I spend at least half an hour reading through documentation. Why bother for the vast majority of my public work just involves stupid things like a slack-bot to render text as party parrots?

Note that while this looks like a lot of code for what it does, it does have a good set of functionality for a small project:

  • Clear precedence of config sources
  • Only basic flag library used
  • Flag for debug output
  • Flag to choose config file (including option to skip file loading without needing a separate flag!)
  • Annotations for env var mapping and file loading of the config are defined by the loader struct itself
  • Default values kept directly above config loader structs for easy comparison
  • No super ugly long parameter sets to pass from flag parsing to initialize structs (builder pattern preferred)
  • Reloadable, so if you create an init ConfigLoader from flags you can then do hot reloads in response to e.g. a SIGHUP (see examples/simple_config/main.go)
  • No magic hidden in a library you need to look up

If you want to try it, it was written so that the precedence is flags > file > env. The default values in the code are { Host: "localhost", Port: 1443 }.

See examples/simple_config/README.md for how to test it out.

State of the art

Currently, some other packages do provide a similar experience. There are a few issues that I have which are not addressed by the ones I am aware of, however.

markphelips/optional
Pros
  • should be somewhat performant due to just using a pointer under the hood
  • ergonomics pretty good
Cons
  • one optional type per underlying type
  • Requires a go generate script to support any non-implemented type
  • Doesn't support some useful features like applying transforms to the value inside the option
  • You could be surprised if you store one of these types in a struct since they are values wrapping pointers, and that would be a nasty debugging session.
leighmcculloch go-optional
Pros
  • Generic so it can be applied to any type
  • short and sweet
  • supports JSON and XML marshaling/unmarshaling
Cons
  • Generic over any, so things like equality can't be supported
  • I don't know if using an internal array is any more efficient than a boolean or pointer to represent Some/None
  • The Marshaling/Unmarshaling uses the zero values to represent None instead of null, removing much of the benefit there
  • Methods like String() uses Sprintf, which uses reflection. There is poor performance, then there is reflection performance.
Magic Values

This is just when you use a specific value to represent your None. Sometimes this makes sense, such as when you have a string field where an empty string would be meaningless.

Pros
  • Easy to use by just defining a const in your package
  • Possibly the most efficient way to do this
Cons
  • You don't always an invalid value to use as your magic value, so it's just impossible sometimes.
  • Ergonomics can get messy; different libraries may return different magic values which you have to translate between
  • I hope you are documenting all of your magic numbers somewhere, because if anyone else looks at your code (including you in 6 months), they probably will have to go code spelunking to understand what is happening.
Pointers
Pros
  • Effective and fast. Just a nil check tells you if a value is set or not.
Cons
  • Nil checks everywhere. It's on you to check for nil before every use, and the consequences of forgetting is a nil pointer dereference panic.
  • Makes for more difficult to read and reason about code at a surprisingly low level of complexity
  • If you every pass a struct by value your invariants can be broken by methods modifying some fields only in the copied struct and others in the copied and original struct. You need to be very careful if you do that.
  • Encourages the "make everything a pointer" style, which encourages wishing you were using another language that doesn't require 10 nil checks in every function.
This package
Pros
  • Ergonomics pretty good. Built with merging values from multiple sources together, marshaling, and templating in mind
  • No surprises
  • Generic implementation means all derivative Option types can be used in the same way
  • Methods for performing transforms on data without extracting it first, which is nice in loops
  • Specialized optional types can be built on top to provide any needed functionality for specific use cases. Look at the file sub-package for an example.
Cons
  • A bit inefficient in terms of space and performance
  • Core Option type is unable to implement some convenient stdlib interfaces due to the use of generics.
  • To make the thing more useful, only comparable values can be wrapped currently. This isn't usually too big of a deal for most values, but does mean that you cannot create an array option for example (but why would you do that?!? Just check for zero length!)

Why? or: the BS configuration manifesto

There are many great things about golang, but being spoiled by choice is not typically one of them.

Not that there is anything wrong with having a good stdlib implementation that can actually be used in prod or there is a lack of good libraries available; I mean there is literally no good way to represent "maybe a thing". If TypeScript can have Optional values, why can't we?

Sure, you can get by in most cases with a pointer. This is, in fact, the most performant way to do this. I do it all the time.

I kept hitting the same few situations, however, where this just caused me grief. The biggest was (is) configuration.

I get that many people feel strongly about configuration. Sometimes just having a quick TOML parser is all you need. Sometimes you are deploying to something like nomad or kubernetes and get better functionality by using env vars. Sometimes you are working at a company that has an in-house configuration system or has standardized on consul and you have no choice. Everyone knows junior devs love nothing more than pre-planning a future outage by using YAML configs and including a float field. And whatever you choose, you will probably also be accepting command-line flags. Flags are just more configuration!

Personally, I don't really care. I find that I have a few goals with configuration, and accomplishing them in the best way usually requires at least flags, env vars, and some kind of config file or networked configuration system.

Flags: Determines which entrypoint to use for code execution and also allows for a human override for any other configuration. Think about your favorite single-binary system like consul, k8s, etc. The flag is what determines if a given execution will run a dev server or production node. It is very clunky to use anything else to do this.

Env vars: Where your code runs is important. What datacenter are you in? What region? Is this production or dev? You could have some system merging the environment configuration with the application configuration, but why? I have seen more than enough puppet or chef configurations trying to handle application config to last a lifetime. But everyone uses orchestration systems like k8s now right? I'm tired of seeing a pod with a handful of side-cars that all have the same environment distilled into their own little files. Humans developed environment variables to solve a real problem, and they are really good at solving those problems!

The wild west: Use YAML, TOML, JSON, BSON, JSON5, Consul, Vault, Zookeeper, etcd, whatever bad distributed key/value store your company implemented on top of redis, or even DynamoDB if you are a psycopath. I'm not gunna sweat it, because if you use flags and env vars properly the only things that cares about this stuff is your application and I, as an dis-interested third party, am never going to have to interact with this. As long as you don't break your own config then ask me to fix it. You can do that, I suppose, but know that some poor person that is taking time out of keeping that old elasticsearch cluster alive for one more day is judging you hard.

Actually, on second thought just use TOML or whatever the company told you to use. You're probably just using it for boolean feature flags, a listening address, and anyways.

FAQ's

What happens if I have a pointer to an Option?

It acts like a pointer to any other value. It is equally useful as a pointer to an int, though, so I wouldn't recommend it.

In practice, I don't find that I actually need pointers to options very often. They are things meant to be calculated once then immutably consumed rather than having a value mutated in a bunch of places. We have some source of truth which we are trying to represent to different parts of our code, marshal and send over the wire, or cache/invalidate a local state of that source. Values work just fine for that.

The one exception would be if you have a very large struct which you want to wrap in an Option. Accepting very large things through a function call may not perform the way you want, in which case you could use a *Option[MyStruct] the same way you might use a *MyStruct.

What about a pointer to a struct which contains an Option?

It acts like any other value.

What about an Option of a pointer?

That's tricky. It has all the same dangers as passing by value a struct with pointer fields. While it is technically possible, I don't recommend it unless you have a very specific need and know your foot guns well.

Be aware that if you do put a pointer inside an Option, nil is a valid value and is NOT None. This means that if you call myOption.IsNone does not tell you if the inner value is a nil pointer.

Additionally, I bet there are complications with marshaling/unmarshaling. Would a json null unmarshal to a None-value Option, or a Some-value Option with the value being a nil pointer? I'm not sure, and I'm not taking the time to think about it.

You could make a specific Pointer derivative of the Option type that handles that sort of thing, but I just don't know what the use case would be where you couldn't just use a pointer on it's own. I certainly don't plan on doing that.

Why didn't you just wrap a pointer then do the right thing? Isn't copying things around by value all the time expensive?
  1. There is a whole world of hurt in golang around structs with pointer fields. If someone is so foolish as to blindly pass such a thing around by value, bad things can happen quickly.
  2. As such, I initially tried to only return pointers to optionals, but given the methods I wanted to provide this didn't always work well.
  3. You can always make an option with a pointer inner type if you want that. It probably isn't totally safe in all cases and there is a very real chance that it won't do what you want. YMMV!

Generating the keys and certs for testing

This is mostly a reminder for myself, given that the certs only have a lifetime of one year.

RSA
openssl genrsa -out tls/rsa/key.pem 4096
openssl rsa -in tls/rsa/key.pem -pubout -out tls/rsa/pubkey.pem
openssl req -new -key tls/rsa/key.pem -x509 -sha256 -nodes -subj "/C=US/ST=California/L=Who knows/O=BS Workshops/OU=optional/CN=www.whobe.us" -days 365 -out tls/rsa/cert.pem
ECDSA
openssl ecparam -name secp521r1 -genkey -noout -out tls/ecdsa/key.pem
openssl ec -in tls/ecdsa/key.pem -pubout > tls/ecdsa/pub.pem
openssl req -new -key tls/ecdsa/key.pem -x509 -sha512 -nodes -subj "/C=US/ST=California/L=Who knows/O=BS Workshops/OU=optional/CN=www.whobe.us" -days 365 -out tls/ecdsa/cert.pem
ED25519
openssl genpkey -algorithm ed25519 -out tls/ed25519/key.pem
openssl pkey -in tls/ed25519/key.pem -pubout -out tls/ed25519/pub.pem
openssl req -new -key tls/ed25519/key.pem -x509 -nodes -subj "/C=US/ST=California/L=Who knows/O=BS Workshops/OU=optional/CN=www.whobe.us" -days 365 -out tls/ed25519/cert.pem

Documentation

Index

Constants

View Source
const DEFAULT_TIME_FORMAT string = time.RFC3339

Variables

This section is empty.

Functions

func And

func And[T comparable, O Optional[T]](left, right O) O

And returns None if the first Optional is None, and the second Optional otherwise. Conceptually similar to left && right. This is a convenience function for Option selection. Convenient for merging configs, implementing builder patterns, etc.

func ClearIfMatch

func ClearIfMatch[T comparable](opt MutableOptional[T], probe T)

ClearIfMatch calls clear if Optional.Match(probe) == true. This is a convenience for situations where you need to convert from a value of T with known "magic value" which implies None. A good example of this is if you have an int loaded from command line flags and you know that any flag omitted by the user will be assigned to 0. This can be done like this: o := Some(x) o.ClearIfMatch(0)

func Equal

func Equal[T comparable, O Optional[T]](left, right O) bool

Equal is a convenience function for checking if the contents of two Optional types are equivilent. Note that Get() and Match() may be overridden by more complex types which wrap a vanilla Option. In these situations, the writer is responsible for making sure that the invariant Some(x).Match(Some(x).Get()) is always true.

func GetOr

func GetOr[T comparable](opt Optional[T], val T) T

GetOr is the same as Get, but will return the passed value instead of an error if the Option is None. Another convenience function

func GetOrElse

func GetOrElse[T comparable](opt Optional[T], f func() T) T

GetOrElse calls Get(), but run the passed function and return the result instead of producing an error if the option is None.

func GetOrInsert

func GetOrInsert[T comparable](opt MutableOptional[T], val T) (T, error)

GetOrInsert calls Get, but will call Default on the passed value then return it if the Option is None

func IsSomeAnd

func IsSomeAnd[T comparable](opt Option[T], f func(T) bool) bool

IsSomeAnd returns true if the Option has a value of Some(x) and f(x) == true

func Or

func Or[T comparable, O Optional[T]](left, right O) O

Or returns the first Optional if it contains a value. Otherwise, return the second Optional. This is conceptually similar to left || right. This is a convenience function for situations like merging configs or implementing builder patterns.

func TransformOr

func TransformOr[T comparable](opt MutableOptional[T], t Transformer[T], backup T) error

TransformOr just calls Transform(), except None values are mapped to backup before being transformed.

Types

type Bool

type Bool struct {
	Option[bool]
}

func NoBool

func NoBool() Bool

func SomeBool

func SomeBool(value bool) Bool

func (Bool) MarshalText

func (o Bool) MarshalText() (text []byte, err error)

func (*Bool) Set

func (o *Bool) Set(str string) error

func (Bool) String

func (o Bool) String() string

func (*Bool) True added in v0.2.1

func (o *Bool) True() bool

True returns true iff the value is Some(true). It is a special method exclusive to Bool optionals, and is the same as calling Bool.Match(true).

func (Bool) Type

func (o Bool) Type() string

func (*Bool) UnmarshalText

func (o *Bool) UnmarshalText(text []byte) error

type Float32

type Float32 struct {
	Option[float32]
}

32bit sized floats

func NoFloat32

func NoFloat32() Float32

func SomeFloat32

func SomeFloat32(value float32) Float32

func (Float32) MarshalText

func (o Float32) MarshalText() (text []byte, err error)

func (*Float32) Set

func (o *Float32) Set(str string) error

func (Float32) String

func (o Float32) String() string

func (Float32) Type

func (o Float32) Type() string

func (*Float32) UnmarshalText

func (o *Float32) UnmarshalText(text []byte) error

type Float64

type Float64 struct {
	Option[float64]
}

64bit sized floats

func NoFloat64

func NoFloat64() Float64

func SomeFloat64

func SomeFloat64(value float64) Float64

func (Float64) MarshalText

func (o Float64) MarshalText() (text []byte, err error)

func (*Float64) Set

func (o *Float64) Set(str string) error

func (Float64) String

func (o Float64) String() string

func (Float64) Type

func (o Float64) Type() string

func (*Float64) UnmarshalText

func (o *Float64) UnmarshalText(text []byte) error

type Int

type Int struct {
	Option[int]
}

default sized int

func NoInt

func NoInt() Int

func SomeInt

func SomeInt(value int) Int

func (Int) MarshalText

func (o Int) MarshalText() (text []byte, err error)

func (*Int) Set

func (o *Int) Set(str string) error

func (Int) String

func (o Int) String() string

func (Int) Type

func (o Int) Type() string

func (*Int) UnmarshalText

func (o *Int) UnmarshalText(text []byte) error

type Int16

type Int16 struct {
	Option[int16]
}

16bit sized int

func NoInt16

func NoInt16() Int16

func SomeInt16

func SomeInt16(value int16) Int16

func (Int16) MarshalText

func (o Int16) MarshalText() (text []byte, err error)

func (*Int16) Set

func (o *Int16) Set(str string) error

func (Int16) String

func (o Int16) String() string

func (Int16) Type

func (o Int16) Type() string

func (*Int16) UnmarshalText

func (o *Int16) UnmarshalText(text []byte) error

type Int32

type Int32 struct {
	Option[int32]
}

32bit sized int

func NoInt32

func NoInt32() Int32

func SomeInt32

func SomeInt32(value int32) Int32

func (Int32) MarshalText

func (o Int32) MarshalText() (text []byte, err error)

func (*Int32) Set

func (o *Int32) Set(str string) error

func (Int32) String

func (o Int32) String() string

func (Int32) Type

func (o Int32) Type() string

func (*Int32) UnmarshalText

func (o *Int32) UnmarshalText(text []byte) error

type Int64

type Int64 struct {
	Option[int64]
}

64bit sized int

func NoInt64

func NoInt64() Int64

func SomeInt64

func SomeInt64(value int64) Int64

func (Int64) MarshalText

func (o Int64) MarshalText() (text []byte, err error)

func (*Int64) Set

func (o *Int64) Set(str string) error

func (Int64) String

func (o Int64) String() string

func (Int64) Type

func (o Int64) Type() string

func (*Int64) UnmarshalText

func (o *Int64) UnmarshalText(text []byte) error

type Int8

type Int8 struct {
	Option[int8]
}

8bit sized int

func NoInt8

func NoInt8() Int8

func SomeInt8

func SomeInt8(value int8) Int8

func (Int8) MarshalText

func (o Int8) MarshalText() (text []byte, err error)

func (*Int8) Set

func (o *Int8) Set(str string) error

func (Int8) String

func (o Int8) String() string

func (Int8) Type

func (o Int8) Type() string

func (*Int8) UnmarshalText

func (o *Int8) UnmarshalText(text []byte) error

type LoadableOptional

type LoadableOptional[T primatives] interface {
	MutableOptional[T]

	// Satisfies fmt.Stringer interface
	String() string
	// Along with String(), implements flag.Value and pflag.Value
	Type() string
	Set(string) error
	// Satisfies the encoding.TextMarshaler
	MarshalText() (text []byte, err error)
	// Satisfies encoding.TextUnmarshaler
	UnmarshalText(text []byte) error
}

LoadableOptional is an extension of the Optional interface meant to make it more useful for loading from a variety of sources.

type MutableOptional

type MutableOptional[T comparable] interface {
	Optional[T]

	MutableClone() MutableOptional[T]
	Clear()
	Default(T) (replaced bool)
	Replace(T) Optional[T]
	// Transform only applies the func to the values of Some valued Optionals. Any mapping of None is None.
	Transform(f Transformer[T]) error
	// Satisfies encoding.json.UnMarshaler
	UnmarshalJSON([]byte) error
}

MutableOptional is a superset of Optional which allows mutating and transforming the wrapped value.

type Option

type Option[T comparable] struct {
	// contains filtered or unexported fields
}

Option is a generic way to make a field or parameter optional. Instantiating an Optional value through the Some() or None() methods are prefered since it is easier for the reader of your code to see what the expected value of the Option is, but to avoid leaking options across api boundries a FromPointer() method is given. This allows you to accept a parameter as a pointer and immediately convert it into an Option value without having to do the nil check yourself.

While this can be useful on its own, this can also be used to create a specialized option for your use case. See the config package for a comprehensive example of this.

Special care should be taken when creating Options of pointer types. All the same concerns around passing structs with pointer fields apply, since copying the pointer by value will create many copies of the same pointer. There is nothing to stop you from doing this, but I'm not sure what they use case is and it may lead to less understandable code (which is what this library was created to avoid in the first place!)

func FromPointer

func FromPointer[T comparable](p *T) Option[T]

FromPointer creates an option with an inferred type from a pointer. nil pointers are mapped to a None value and non-nil pointers have their value copied into a new option. The pointer itself is not retained and can be modified later without affecting the Option value.

func None

func None[T comparable]() Option[T]

None returns an Option with no value loaded. Note that since there is no value to infer type from, None must be instanciated with the desired type like optional.None[string]().

func Some

func Some[T comparable](value T) Option[T]

Some returns an Option with an inferred type and specified value.

func (*Option[T]) Clear

func (o *Option[T]) Clear()

Clear converts a Some(x) or None type Option into a None value.

func (Option[T]) Clone

func (o Option[T]) Clone() Optional[T]

Clone creates a copy of the Option by value. This means the new Option may be unwrapped or modified without affecting the old Option. NOTE: the exception to this would be if you create an Option for a pointer type. Because the pointer is copied by value, it will still refer to the same value.

func (*Option[T]) Default added in v0.2.0

func (o *Option[T]) Default(value T) (replaced bool)

Default is a special case of replace where the value of the option is only changed if the current value is None.

func (Option[T]) Get

func (o Option[T]) Get() (val T, ok bool)

Get returns the current wrapped value of a Some value Option and an ok value indicating if the Option was Some or None. Note that the wrapped value returned if ok == false if undefined so ALWAYS CHECK.

func (Option[T]) IsNone

func (o Option[T]) IsNone() bool

func (Option[T]) IsSome

func (o Option[T]) IsSome() bool

func (Option[T]) MarshalJSON

func (o Option[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements the encoding.json.Marshaler interface. None values are marshaled to json null, while Some values are passed into json.Marshal directly.

func (Option[T]) Match

func (o Option[T]) Match(probe T) bool

Match tests if the inner value of Option == the passed value

func (Option[T]) MustGet added in v0.2.0

func (o Option[T]) MustGet() T

MustGet is exactly like get, but panics for None instead of returning an error. This makes for potentially more readable code if paired with Option.IsSome or in a template and gives an exciting sense of danger.

func (Option[T]) MutableClone

func (o Option[T]) MutableClone() MutableOptional[T]

MutableClone creates a copy of the Option by value the same as Clone. The only differnce is that the returned type is a pointer cast as a MutableOptional so that the returned value can be further modified.

func (*Option[T]) Replace

func (o *Option[T]) Replace(value T) Optional[T]

Replace converts a Some(x) or None type Option into a Some(value) value.

func (*Option[T]) Transform

func (o *Option[T]) Transform(t Transformer[T]) error

Transform applies function f(T) to the inner value of the Option. If the Option is None, then the Option will remain None.

func (*Option[T]) UnmarshalJSON

func (o *Option[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON implements the encoding.json.Unmarshaller interface. Json nulls are unmarshaled into None values, while any other value is attempted to unmarshal as normal. Any error encountered is returned without modification. There are some protections included to make sure that unmarshaling an uninitialized Option[T] does not break the Option invariants.

type Optional

type Optional[T comparable] interface {
	IsSome() bool
	IsNone() bool
	Clone() Optional[T]
	Get() (val T, ok bool)
	MustGet() T
	Match(T) bool
	// Satisfies encoding.json.Marshaler
	MarshalJSON() ([]byte, error)
}

Optional defines the functionality needed to provide good ergonimics around optional fields and values. In general, code should not declare variables or parameters as Optionals and instead prefer using concrete types like Option. This interface is meant to ensure compatablility between different concrete option types and for the rare cases where the abstraction is actually necessary.

type OptionalError

type OptionalError struct {
	// contains filtered or unexported fields
}

func (OptionalError) Error

func (e OptionalError) Error() string

type Secret added in v0.2.2

type Secret struct {
	Str
}

Secret wraps another Optional which may hold sensitive information. The value may still be marshaled into messages, but when printing to the console or logs it should be redacted.

func MakeSecret added in v0.2.2

func MakeSecret(str *Str) Secret

MakeSecret creates a Secret from a pointer to Str optional then clears the original. This is to ensure the original optional is never logged, thereby leaking the secret.

func NoSecret added in v0.2.2

func NoSecret() Secret

func SomeSecret added in v0.2.2

func SomeSecret(value string) Secret

func (Secret) Format added in v0.2.2

func (s Secret) Format(f fmt.State, verb rune)

ALWAYS ALWAYS redact secrets no matter what formatting verb or flag is set

func (Secret) LogValue added in v0.2.2

func (s Secret) LogValue() slog.Value

Even redact secrets when logging. What a surprise!

func (Secret) String added in v0.2.2

func (s Secret) String() string

ALWAYS redact secrets

func (Secret) Type added in v0.2.2

func (s Secret) Type() string

Override the Type() method from the inner string. Part of the flag.Value interface.

type Str

type Str struct {
	Option[string]
}

Str implements Configand for the string type.

func NoStr

func NoStr() Str

func SomeStr

func SomeStr(value string) Str

func (Str) MarshalText

func (o Str) MarshalText() (text []byte, err error)

func (*Str) Set

func (o *Str) Set(str string) error

func (Str) String

func (o Str) String() string

func (Str) Type

func (o Str) Type() string

func (*Str) UnmarshalText

func (o *Str) UnmarshalText(text []byte) error

type Time

type Time struct {
	Option[time.Time]
	// contains filtered or unexported fields
}

func NoTime

func NoTime(formats ...string) Time

func SomeTime

func SomeTime(value time.Time, formats ...string) Time

func (Time) Formats

func (o Time) Formats() []string

func (Time) MarshalJSON

func (o Time) MarshalJSON() ([]byte, error)

func (Time) MarshalText

func (o Time) MarshalText() (text []byte, err error)

func (*Time) Set

func (o *Time) Set(str string) error

func (Time) String

func (o Time) String() string

func (Time) Type

func (o Time) Type() string

func (*Time) UnmarshalJSON

func (o *Time) UnmarshalJSON(data []byte) error

Unmarshaller interface

func (*Time) UnmarshalText

func (o *Time) UnmarshalText(text []byte) error

func (Time) WithFormats

func (o Time) WithFormats(formats ...string) Time

type Transformer

type Transformer[T comparable] func(T) (T, error)

type Uint

type Uint struct {
	Option[uint]
}

default sized uint

func NoUint

func NoUint() Uint

func SomeUint

func SomeUint(value uint) Uint

func (Uint) MarshalText

func (o Uint) MarshalText() (text []byte, err error)

func (*Uint) Set

func (o *Uint) Set(str string) error

func (Uint) String

func (o Uint) String() string

func (Uint) Type

func (o Uint) Type() string

func (*Uint) UnmarshalText

func (o *Uint) UnmarshalText(text []byte) error

type Uint16

type Uint16 struct {
	Option[uint16]
}

16bit sized uint

func NoUint16

func NoUint16() Uint16

func SomeUint16

func SomeUint16(value uint16) Uint16

func (Uint16) MarshalText

func (o Uint16) MarshalText() (text []byte, err error)

func (*Uint16) Set

func (o *Uint16) Set(str string) error

func (Uint16) String

func (o Uint16) String() string

func (Uint16) Type

func (o Uint16) Type() string

func (*Uint16) UnmarshalText

func (o *Uint16) UnmarshalText(text []byte) error

type Uint32

type Uint32 struct {
	Option[uint32]
}

32bit sized uint

func NoUint32

func NoUint32() Uint32

func SomeUint32

func SomeUint32(value uint32) Uint32

func (Uint32) MarshalText

func (o Uint32) MarshalText() (text []byte, err error)

func (*Uint32) Set

func (o *Uint32) Set(str string) error

func (Uint32) String

func (o Uint32) String() string

func (Uint32) Type

func (o Uint32) Type() string

func (*Uint32) UnmarshalText

func (o *Uint32) UnmarshalText(text []byte) error

type Uint64

type Uint64 struct {
	Option[uint64]
}

64bit sized uint

func NoUint64

func NoUint64() Uint64

func SomeUint64

func SomeUint64(value uint64) Uint64

func (Uint64) MarshalText

func (o Uint64) MarshalText() (text []byte, err error)

func (*Uint64) Set

func (o *Uint64) Set(str string) error

func (Uint64) String

func (o Uint64) String() string

func (Uint64) Type

func (o Uint64) Type() string

func (*Uint64) UnmarshalText

func (o *Uint64) UnmarshalText(text []byte) error

type Uint8

type Uint8 struct {
	Option[uint8]
}

8bit sized uint

func NoUint8

func NoUint8() Uint8

func SomeUint8

func SomeUint8(value uint8) Uint8

func (Uint8) MarshalText

func (o Uint8) MarshalText() (text []byte, err error)

func (*Uint8) Set

func (o *Uint8) Set(str string) error

func (Uint8) String

func (o Uint8) String() string

func (Uint8) Type

func (o Uint8) Type() string

func (*Uint8) UnmarshalText

func (o *Uint8) UnmarshalText(text []byte) error

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL