Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: syntactic sugar for error handling (sharp guard) #57052

Closed
protogrammer opened this issue Dec 2, 2022 · 5 comments
Closed

proposal: Go 2: syntactic sugar for error handling (sharp guard) #57052

protogrammer opened this issue Dec 2, 2022 · 5 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@protogrammer
Copy link

Author background

  • Student, a little experience with go, had built a telegram bot with it, but already quite tired of if err != nil { ... }
  • Wrote some code in C, C++, Python and typescript, pretty familiar with Java and Haskell

Related proposals

  • I could not find the same idea after a brief search
    • most of proposals rely on creating additional keywords and do not assume keeping in mind that error may be not only object returned by a function

Proposal

  • I propose the next syntactic sugar for error handling.
    It's rather simple, helps to reduce repeated code, and seems back-compatible
    I'm not sure I'm able to give detailed specs, but I'll just show a simple example
func HexStringToDecimal(s string) (string, error) {
  x, #("", _) := strconv.ParseUint(s, 16, 64)
  return strconv.FormatUint(x, 10)
}
  • It is similar to
func HexStringToDecimal(s string) (string, error) {
  x, err := strconv.ParseUint(s, 16, 64)
  if err != nil {
    return "", err
  }
  return strconv.FormatUint(x, 10)
}
  • So, what does # ... syntax mean? It can appears only on the left side of operators = and :=. When it gets an error err (it probably can get not only error, I'll write about it below), it checks whether err is nil. If it is, it works just like _. But if it's not, function returns the values given in parenthesis. If function return only one value, parenthesis are optional, so if the only return value has type error, error handling can be as simple as file, #_ := os.Create("some-file"). If function returns no value, probably it should look like #().
    Let's call this syntax sharp guard
    The underline _ inside #-guard is the value passed from the right side of an expression. Probably, we should choose another special sign or keyword for it to escape a confusion with underline used to ignore a value
    Keep in mind, sharp guards can take any expressions as well. Also it's reasonable to provide "partial" sharp guard syntax when return values are named. So, this code may become possible:
func WriteArrayToFile(arr []string, filename string) (writtenElements uint, err error) {
  f, #(err: _) := os.Create(filename)
  defer func() {
    #panic(_) = f.Close()
  }()
  
  w := bufio.NewWriter(f)
  defer func() {
    #panic(_) = w.Flush()
  }()

  for _, s := range arr {
    #(err: _) = w.WriteString(s)
    writtenElements++
    #(err: _) = w.WriteRune('\n')
  }

  return
}
  • That will be equal to:
func WriteArrayToFile(arr []string, filename string) (writtenElements uint, err error) {
  var f *os.File
  f, err = os.Create(filename)
  if err != nil {
    return
  }
  defer func() {
    if err := f.Close(); err != nil {
      panic(err)
    }
  }()
  
  w := bufio.NewWriter(f)
  defer func() {
    if err := w.Flush(); err != nil {
      panic(err)
    }
  }()

  for _, s := range arr {
    err = w.WriteString(s)
    if err != nil {
      return
    }
    writtenElements++
    err = w.WriteRune('\n')
    if err != nil {
      return
    }
  }

  return
}

Probably, #panic(...) here is a special case. It can be used in functions with any number of return values

  • Also it's possible to write some wrappers for errors
type PhaseError struct {
  Phase string
  Err error
}

func (err *PhaseError ) Error() string {
  return fmt.Sprintf("error with %s: %v", err.Phase , err.Err)
}

func wrapper(phase string, err error) (*Data, *PhaseError) {
  return nil, &PhaseError {
    Phase: phase,
    Err: err,
  }
}

func DoSomeComplicatedStuff(url string) (*Data, *PhaseError) {
  var client http.Client
  client.Timeout = consts.Timeout
  resp, #wrapper("loading", _) := client.Get(url)
  data, #wrapper("processing", _) := processRecievedResponse(resp.Body)
  #wrapper("storing", _) = database.Store(data)
  return data, nil
}
  • That should be similar to
type PhaseError struct {
  Phase string
  Err error
}

func (err *PhaseError ) Error() string {
  return fmt.Sprintf("error with %s: %v", err.Phase , err.Err)
}

func wrapper(phase string, err error) (*Data, *PhaseError) {
  return nil, &PhaseError {
    Phase: phase,
    Err: err,
  }
}

func DoSomeComplicatedStuff(url string) (*Data, *PhaseError) {
  var client http.Client
  client.Timeout = consts.Timeout
  resp, err := client.Get(url)
  if err != nil {
    return wrapper("loading", err)
  }
  data, err := processRecievedResponse(resp.Body)
  if err != nil {
    return wrapper("processing", err)
  }
  err = database.Store(data)
  if err != nil {
    return wrapper("storing", err)
  }
  return data, nil
}
  • Should sharp guards take only errors? Actually, the initial idea was to use sharp guard for any emptiable values. But here is a problem: are empty values always non-error values? For instance, for extract values by key from map, we will receive a boolean which is true when value is present and false when it is absent. So, such code might be a confusion:
value, #panic("Occasionally, value is present!") := someMap[key]

Probably, boolean must be an exception, but in my opinion using error as the only type for sharp guards is the most solid decision.

  • What if we put several sharp guards in one line? Nothing good, but I see no reason for prohibiting it

Costs

  • I don't think this will make Go much harder to learn
  • Probably no tools will be affected
  • I think compile time won't be affected a lot
  • Runtime cost is none
    I don't really understand the costs well. Participating in this discussion is welcome.
@gopherbot gopherbot added this to the Proposal milestone Dec 2, 2022
@seankhliao seankhliao added LanguageChange v2 A language change or incompatible library change error-handling Language & library change proposals that are about error handling. labels Dec 2, 2022
@seankhliao
Copy link
Member

Duplicate of #32473

@seankhliao seankhliao marked this as a duplicate of #32473 Dec 2, 2022
@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Dec 2, 2022
@protogrammer
Copy link
Author

protogrammer commented Dec 3, 2022

But it actually isn't a duplicate 🤔 sharp guard syntax doesn't suppose creating some possibilities like return^2, it just means that if error is not nil it will return specific values or panic, or call a function that returns the same values as the function it was called from

@protogrammer
Copy link
Author

It's much more concrete and clear that syntax supposed in #32473

@seankhliao
Copy link
Member

see also #43644 #52416

@protogrammer
Copy link
Author

Also it doesn't suppose to be an alternative to try/catch, it's just a syntactic sugar

@golang golang locked and limited conversation to collaborators Dec 5, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

3 participants