I'd like to understand why one would create a type for handling errors in Go and how you decide what underlying type it should have

I'm working through A Tour of Go - Exercise: Errors. It holds my hand as I add error handling to a square root function.

Here is my solution:

package main

import (
    "fmt"
    "math"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
    fmt.Sprint(float64(e))
    return fmt.Sprintf("cannot Sqrt negative number: %g", float64(e))
}

func Sqrt(x float64) (float64, error) {
    z := 1.0
    margin := 0.00000000000001
    for {
        if x < 0 {
            return 0, ErrNegativeSqrt(x)
        }
        previousZ := z
        z = z - (z*z-x)/(2*z)
        if math.Abs(previousZ-z) < margin {
            fmt.Println(previousZ, "-", z, "=", previousZ-z)
            break
        }
    }
    fmt.Println("math.Sqrt:", math.Sqrt(x))
    return z, nil
}

func main() {
    fmt.Println(Sqrt(2))
    fmt.Println(Sqrt(-2))
}

I am having trouble understanding the line:

type ErrNegativeSqrt float64

I have two questions:

  1. Why is the underlying type of ErrNegativeSqrt "float64" and not "error"?

and

  1. Why create a type in the first place? Why do we create an error type and add a method to it instead of just having a free-standing function?

I'm a beginner at Go and I'd like to understand. Thank you so much.

Answers


I'm gonna answer your second question first: you add a method to your type so ErrNegativeSqrt implements the error interface, which means other code that does not know about the specifics of ErrNegativeSqrt can use it as any other error.

As the error interface only requires a method Error() with return type string, a common type to use is just a string with the Error method returning that string (defined in package "errors").

I don't know why it was decided to use float64 as the error type in this case, but the main advantage that I see is that if you had several functions that could return a similar error, you'd have to do the following:

func Method1() error{
  ...
  return errors.New(fmt.Sprintf("cannot Sqrt negative number: %g", number)
}

func Method2() error{
  ...
  return errors.New(fmt.Sprintf("cannot Sqrt negative number: %g", number2)
}

And repeat for any function that needs to return a similar thing. With the code that you posted, you only declare the number responsible for the error when you create it, and the method "Error()" returns the formated string for all.

func Method1() error{
  ...
  ErrNegativeSqrt(number)
}

func Method2() error{
  ...
  ErrNegativeSqrt(number)
}

One usually doesn't create custom error types, but this is a demonstration of how errors work, as well as some other Go concepts.

Why is the underlying type of ErrNegativeSqrt "float64" and not "error"?

This allows the float64 value in Sqrt to be directly converted to an error. It also demonstrates how new types can have various underlying types, and methods can be attached to types other than structs. If you need to create your own type of error, use the underlying type that is most convenient for your use case.

Why create a type in the first place? Why do we create an error type and add a method to it instead of just having a free-standing function?

Sometimes an error needs to have more information or context than can be contained in the single string returned from errors.New, or fmt.Errorf. For example, the net package uses a net.OpError (which conforms to the net.Error interface) to package information about addresses, whether it's a temporary error, or a caused by a timeout.

In most cases a simple string-based error will suffice.


Like any other languages, we create a type or object to make sense and group functionalities for our own sakes (computer doesn't care). In Go, it is common to create a custom type or a struct type to extend the functionalities of an object.

In this case, creating a type ErrNegativeSqrt you can actually extend a float64 functionalities by adding methods. For instance, compare this two approaches:

func TimesTwo(val int) int {
        return val * 2
}

with

type SuperChargedInt int

func (sc SuperChargedInt) TimesTwo() int {
        return int(sc) * 2
}

Instead of passing an int to a function, a SuperChargedInt has the capability to act on itself. This becomes very handy when you tread into interfaces.

type MyInt interface {
        TimesTwo() int
}

type IntA int
type IntB int

func (a IntA) TimesTwo() int {
        return int(a) * 2
}

func (b IntB) TimesTwo() int {
        return int(b) * (1 + 1)
}

func PrintMyInt(c MyInt) {
        fmt.Println(c)
}

Type IntA and IntB, satisfying the interface MyIntbecause they both implement the required methods, can be use anywhere in the code that requires MyInt.

var a IntA = 34
var b IntB = 32
PrintMyInt(a)
PrintMyInt(b)

type SomeNum struct {
        A MyInt
}

num := SomeNum{ A: a }

With the understanding of interfaces, now coming back to ErrNegativeSqrt type, adding Error() method to it make the type implements the error interface (which has only one method Error() to implement). Now you can use the type just like an error anywhere.

This is the duck-typing in Golang.


Need Your Help

Neo4J - SpringData - @query using compound query

neo4j spring-data spring-data-neo4j

I'm kind of new to Neo4J: I've built an application that uses Neo4j using SpringData.

Future of the SAP RFC SDK

.net sdk sap saprfc

Is the SAP RFC SDK (wdtfuncs.ocx, wdtlog.ocx respectively Interop.SAPFunctionsOCX.dll, Interop.SAPLogonCtrl.dll) an acceptable / recommended way to connect (Microsoft) applications via RFCs with SA...