9

I have a list of events (enum) which defines the particular event:

package events

const (
    NEW_USER       = "NEW_USER"
    DIRECT_MESSAGE = "DIRECT_MESSAGE"
    DISCONNECT     = "DISCONNECT"
)

And there is a struct that will use this enum as one of its attribute

type ConnectionPayload struct {
    EventName    string      `json:"eventName"`
    EventPayload interface{} `json:"eventPayload"`
}

Is there a way I can use enum as a type for EventName instead of string?

This is possible in typescript not sure how to do it in golang.

I want the developers to forcefully use correct event name using the enum instead of making a mistake by using any random string for eventname.

Karan Kumar
  • 2,678
  • 5
  • 29
  • 65
  • The Ada language requires an enumeration to be a member of a type. For instance, type colors is (red, orange, yellow, green, blue, indigo, violet); The enumeration values are not Ada strings. – Jim Rogers Apr 20 '22 at 04:26
  • Sorry could you please explain? – Karan Kumar Apr 20 '22 at 04:30
  • Not with a string, no. There are ways to force using the correct values, but these are unnecessary in most contexts. For instance, you can define an interface `EventType` containing an unexported method, define an unexported type based on string implementing that interface, and define enum values as instances of that type. – Burak Serdar Apr 20 '22 at 04:33
  • Suggestion 1: https://github.com/abice/go-enum – Tiago Peczenyj Apr 20 '22 at 06:11
  • Suggestion 2 (I use this one, very often) https://github.com/dmarkham/enumer – Tiago Peczenyj Apr 20 '22 at 06:12
  • First, define your enumerations, the use enumerator generator. Use a type derived from int. If needed you can convert from/to string. If the string is not recognized it will fail at runtime. – Tiago Peczenyj Apr 20 '22 at 06:16
  • But you can’t force a string variable accept only few values at compile time. You can mitigate by using constants of a given type – Tiago Peczenyj Apr 20 '22 at 06:18
  • For restricting type's values, see [Creating a Constant Type and Restricting the Type's Values](https://stackoverflow.com/questions/37385007/creating-a-constant-type-and-restricting-the-types-values/37386119#37386119) – icza Apr 20 '22 at 07:17

4 Answers4

13

There is no enum type at the moment in go, and there currently isn't a direct way to enforce the same rules as what typescript does.


A common practice in go is to use the suggestion posted by @ttrasn :

define a custom type, and typed constants with your "enum" values :

type EventName string

const (
    NEW_USER       EventName = "NEW_USER"
    DIRECT_MESSAGE EventName = "DIRECT_MESSAGE"
    DISCONNECT     EventName = "DISCONNECT"
)

This allows you to flag, in your go code, the places where you expect such a value :

// example function signature :
func OnEvent(e EventName, id int) error { ... }

// example struct :
type ConnectionPayload struct {
    EventName    EventName  `json:"eventName"`
    EventPayload interface{} `json:"eventPayload"`
}

and it will prevent assigning a plain string to an EventName

var str string = "foo"
var ev EventName

ev = str // won't compile
OnEvent(str, 42) // won't compile

The known limitations are :

  • in go, there is always a zero value :
    var ev EventName  // ev is ""
    
  • string litterals are not the same as typed variables, and can be assigned :
    var ev EventName = "SOMETHING_ELSE"
    
  • casting is allowed :
    var str string = "foo"
    var ev EventName = EventName(str)
    
  • there is no check on unmarshalling :
    jsn := []byte(`{"eventName":"SOMETHING_ELSE","eventPayload":"some message"}`)
    err := json.Unmarshal(jsn, &payload) // no error
    

https://go.dev/play/p/vMUTpvH8DBb

If you want some stricter checking, you would have to write a validator or a custom unmarshaler yourself.

LeGEC
  • 46,477
  • 5
  • 57
  • 104
  • Would it be possible in golang to get the EventName values in this type? Like in typescript Object.keys(EventName)? – Martin Apr 16 '23 at 08:48
9

You can do it by generating code like the below.


type EventNames string

const (
    NEW_USER       EventNames = "NEW_USER"
    DIRECT_MESSAGE EventNames = "DIRECT_MESSAGE"
    DISCONNECT     EventNames = "DISCONNECT"
)

then change your struct to this:

type ConnectionPayload struct {
    EventName    EventNames  `json:"eventName"`
    EventPayload interface{} `json:"eventPayload"`
}
ttrasn
  • 4,322
  • 4
  • 26
  • 43
  • 2
    I just tried this, this give intellisense for valid values, but it doesn't throw any error if I assign a random string to "EventName" – Karan Kumar Apr 20 '22 at 04:58
  • @KaranKumar For that, you can create a `validator method` as Thuc said after my answer. – ttrasn Apr 20 '22 at 05:09
3

Try this:

package main

import (
    "fmt"

    "gopkg.in/go-playground/validator.v9"
)

type EventName string

const (
    NEW_USER       EventName = "NEW_USER"
    DIRECT_MESSAGE EventName = "DIRECT_MESSAGE"
    DISCONNECT     EventName = "DISCONNECT"
)

type ConnectionPayload struct {
    EventName    EventName   `json:"eventName" validate:"oneof=NEW_USER DIRECT_MESSAGE DISCONNECT"`
    EventPayload interface{} `json:"eventPayload"`
}

func (s *ConnectionPayload) Validate() error {
    validate := validator.New()
    return validate.Struct(s)
}

func main() {
    a := ConnectionPayload{
        EventName: "NEW_USER",
    }
    b := ConnectionPayload{
        EventName: "NEW_USERR",
    }
    err := a.Validate()
    fmt.Println(a, err)
    err = b.Validate()
    fmt.Println(b, err)
}
Thuc Nguyen
  • 235
  • 2
  • 8
0

This maybe little bit out-of-scope but I was searching for something OP mentioned at the bottom of the question:

I want the developers to forcefully use correct event name.

But during research I found out that forcefully sealing it is not something that should be done.

DISCLAIMER:
This should not be done for enum types in GO. GO taught me that you should be responsible when you program and you shouldn't do something even if you're allowed to like create new instance of enum. It is far better to use solution @ttrasn proposed for this specific case.

But to the point... Go allows you to seal an interface by creating public, exported interface type with internal, non-exportable method. This interface is sealed because there is only single package that is able to implement it and that package is the one where the interface is declared.

type EventName interface {
    eventName_internal()
}


type eventName string
func (eventName) eventName_internal() {}

const (
    NEW_USER       eventName = "NEW_USER"
    DIRECT_MESSAGE eventName = "DIRECT_MESSAGE"
    DISCONNECT     eventName = "DISCONNECT"
)

But instead of using this pattern for enums this pattern is usually used for when you need to accept finite set of implementation for specific interface. Although this is great example to show how to seal a type and having same functionality as from other programming languages.

vbargl
  • 649
  • 8
  • 13