This question already has answers here:
Compare error message in golang
(2 answers)
Closed 1 year ago.
Context: I am an amateur dev coming from Python and I am just starting with Go
(Python background to show how I manage errors today)
In Python, the typical way to raise exceptions (errors) with your own "error type" is
class noMoreBeer(Exception):
pass
try:
a_function()
except noMoreBeer as e:
print("there is no more beer ({e}), go buy some")
except Exception as e:
print(f"unexpected exception happened, namely {e}")
else:
print("thinks went fine")
The main part I would like to port to Go philosophy is that I created my own exception which can have optional explanation text, but I check for noMoreBeer, not the error text.
Now back to Go, I read several pages on how to handle errors (while I was annoyed first, I now find that it makes for better code), among them on the Go Blog. Out of this I tried to replicate the above, but the code below does not work (JetBrain's Goland points to return Error.noMoreBeer() and if err == Error.noMoreBeer())
package main
import "fmt"
type Error interface {
error
noMoreBeer() bool
}
func thisFails() Error {
// returns my specific error, but could also percolate some other errors up
return Error.noMoreBeer()
}
func main() {
err := thisFails()
if err != nil {
if err == Error.noMoreBeer() {
fmt.Println("go buy some beer")
} else {
panic("something unexpected happened")
}
}
}
Is there a way in Go to create such specific errors?
One of the main drivers for them in my case is that I do not rely on the text passed in the error, but on a [class|whatever] which, if it has a typo, will be an error.
There's four functions you should look into. Here's the official Go blog post for this subject. It was introduced in Go 1.13.
fmt.Errorf: Create a new error with some details. Can wrap errors with %w.
errors.New: Create a new error type that can be wrapped and compared with functions introduced in Go 1.13.
errors.Is: Compare an error variable with an error type. It can unwrap errors.
errors.As: Compare an error variable with an error interface implementation. It can unwrap errors.
My first shot at a solution:
package main
import (
"errors"
"fmt"
)
var noMoreBeer = errors.New("no more beer")
func thisFails() error {
// returns my specific error, but could also percolate some other errors up
return noMoreBeer
}
func main() {
err := thisFails()
if err != nil {
if err == noMoreBeer {
fmt.Println("go buy some beer")
} else {
panic("something unexpected happened")
}
}
}
The key point I understood reading https://blog.golang.org/go1.13-errors is that I can test against a variable which type is error. This is functionally equivalent to the Python example I started with.
You could use a type switch.
package main
import "fmt"
// the interface
type NoMoreBeerError interface {
noMoreBeer() bool
}
// two different implementations of the above interface
type noMoreBeerFoo struct { /* ... */ }
type noMoreBeerBar struct { /* ... */ }
func (noMoreBeerFoo) noMoreBeer() bool { return true }
func (noMoreBeerBar) noMoreBeer() bool { return false }
// have the types also implement the standard error interface
func (noMoreBeerFoo) Error() string { return " the foo's error message " }
func (noMoreBeerBar) Error() string { return " the bar's error message " }
func thisFails() error {
if true {
return noMoreBeerFoo{}
} else if false {
return noMoreBeerFoo{}
}
return fmt.Errorf("some other error")
}
func main() {
switch err := thisFails().(type) {
case nil: // all good
case NoMoreBeerError: // my custom error type
if err.noMoreBeer() { // you can invoke the method in this case block
// ...
}
// if you need to you can inspect farther for the concrete types
switch err.(type) {
case noMoreBeerFoo:
// ...
case noMoreBeerBar:
// ...
}
default:
// handle other error type
}
}
We have been using errors by cutome implementation of error interface in production without any issues. This is helping detect the error and find root cause more efficiently.
package errors
type Error struct {
err error // the wrapped error
errType ErrorType
errInfo ErrorInfo
op Op
// domain specific data
userID int
requestID string
}
type (
ErrorType string
ErrorInfo string
Op string
)
var NoMoreBeer ErrorType = "NoMoreBeerError"
func (e Error) Error() string {
return string(e.errInfo)
}
func Encode(op Op, args ...interface{}) error {
e := Error{}
for _, arg := range args {
switch arg := arg.(type) {
case error:
e.err = arg
case ErrorInfo:
e.errInfo = arg
case ErrorType:
e.errType = arg
case Op:
e.op = arg
case int:
e.userID = arg
case string:
e.requestID = arg
}
}
return e
}
func (e Error) GetErrorType() ErrorType {
return e.errType
}
By this, you can use errors as you'd use them but adding the capabilities of a expansion as needed.
e := errors.Encode(
"pkg.beerservice.GetBeer",
errors.NoMoreBeer,
errors.ErrorInfo("No more beer available"),
).(errors.Error)
switch e.GetErrorType() {
case errors.NoMoreBeer:
fmt.Println("no more beer error")
fmt.Println(e.Error()) // this will return "No more beer available"
}
Working example on playground: https://play.golang.org/p/o9QnDOzTwpc
In Python, you can do this:
"File {file} had error {error}".format(file=myfile, error=err)
or this:
"File %(file)s had error %(error)s" % {"file": myfile, "error": err}
In Go, the simplest option is:
fmt.Sprintf("File %s had error %s", myfile, err)
which doesn't let you swap the order of the parameters in the format string, which you need to do for I18N. Go does have the template package, which would require something like:
package main
import (
"bytes"
"text/template"
"os"
)
func main() {
type Params struct {
File string
Error string
}
var msg bytes.Buffer
params := &Params{
File: "abc",
Error: "def",
}
tmpl, _ := template.New("errmsg").Parse("File {{.File}} has error {{.Error}}")
tmpl.Execute(&msg, params)
msg.WriteTo(os.Stdout)
}
which seems like a long way to go for an error message. Is there a more reasonable option that allows me to give string parameters independent of order?
With strings.Replacer
Using strings.Replacer, implementing a formatter of your desire is very easy and compact.
func main() {
file, err := "/data/test.txt", "file not found"
log("File {file} had error {error}", "{file}", file, "{error}", err)
}
func log(format string, args ...string) {
r := strings.NewReplacer(args...)
fmt.Println(r.Replace(format))
}
Output (try it on the Go Playground):
File /data/test.txt had error file not found
We can make it more pleasant to use by adding the brackets to the parameter names automatically in the log() function:
func main() {
file, err := "/data/test.txt", "file not found"
log2("File {file} had error {error}", "file", file, "error", err)
}
func log2(format string, args ...string) {
for i, v := range args {
if i%2 == 0 {
args[i] = "{" + v + "}"
}
}
r := strings.NewReplacer(args...)
fmt.Println(r.Replace(format))
}
Output (try it on the Go Playground):
File /data/test.txt had error file not found
Yes, you could say that this only accepts string parameter values. This is true. With a little more improvement, this won't be true:
func main() {
file, err := "/data/test.txt", 666
log3("File {file} had error {error}", "file", file, "error", err)
}
func log3(format string, args ...interface{}) {
args2 := make([]string, len(args))
for i, v := range args {
if i%2 == 0 {
args2[i] = fmt.Sprintf("{%v}", v)
} else {
args2[i] = fmt.Sprint(v)
}
}
r := strings.NewReplacer(args2...)
fmt.Println(r.Replace(format))
}
Output (try it on the Go Playground):
File /data/test.txt had error 666
A variant of this to accept params as a map[string]interface{} and return the result as a string:
type P map[string]interface{}
func main() {
file, err := "/data/test.txt", 666
s := log33("File {file} had error {error}", P{"file": file, "error": err})
fmt.Println(s)
}
func log33(format string, p P) string {
args, i := make([]string, len(p)*2), 0
for k, v := range p {
args[i] = "{" + k + "}"
args[i+1] = fmt.Sprint(v)
i += 2
}
return strings.NewReplacer(args...).Replace(format)
}
Try it on the Go Playground.
With text/template
Your template solution or proposal is also way too verbose. It can be written as compact as this (error checks omitted):
type P map[string]interface{}
func main() {
file, err := "/data/test.txt", 666
log4("File {{.file}} has error {{.error}}", P{"file": file, "error": err})
}
func log4(format string, p P) {
t := template.Must(template.New("").Parse(format))
t.Execute(os.Stdout, p)
}
Output (try it on the Go Playground):
File /data/test.txt has error 666
If you want to return the string (instead of printing it to the standard output), you may do it like this (try it on the Go Playground):
func log5(format string, p P) string {
b := &bytes.Buffer{}
template.Must(template.New("").Parse(format)).Execute(b, p)
return b.String()
}
Using explicit argument indices
This was already mentioned in another answer, but to complete it, know that the same explicit argument index may be used arbitrary number of times and thus resulting in the same parameter substituted in multiple times. Read more about this in this question: Replace all variables in Sprintf with same variable
I don't know of any easy way of naming the parameters, but you can easily change the order of the arguments, using explicit argument indexes:
From docs:
In Printf, Sprintf, and Fprintf, the default behavior is for each formatting verb to format successive arguments passed in the call. However, the notation [n] immediately before the verb indicates that the nth one-indexed argument is to be formatted instead. The same notation before a '*' for a width or precision selects the argument index holding the value. After processing a bracketed expression [n], subsequent verbs will use arguments n+1, n+2, etc. unless otherwise directed.
Then you can, ie:
fmt.Printf("File %[2]s had error %[1]s", err, myfile)
The parameter can also be a map, so the following function would work if you don't mind parsing every error format every time you use it:
package main
import (
"bytes"
"text/template"
"fmt"
)
func msg(fmt string, args map[string]interface{}) (str string) {
var msg bytes.Buffer
tmpl, err := template.New("errmsg").Parse(fmt)
if err != nil {
return fmt
}
tmpl.Execute(&msg, args)
return msg.String()
}
func main() {
fmt.Printf(msg("File {{.File}} has error {{.Error}}\n", map[string]interface{} {
"File": "abc",
"Error": "def",
}))
}
It's still a little wordier than I would have liked, but it's better than some other options, I suppose. You could turn map[string]interface{} into a local type and reduce it further to:
type P map[string]interface{}
fmt.Printf(msg("File {{.File}} has error {{.Error}}\n", P{
"File": "abc",
"Error": "def",
}))
Alas, there's no built-in function in Go for string interpolation with named parameters (yet). But you are not the only one suffering out there :) Some packages should exist, for example: https://github.com/imkira/go-interpol . Or, if feeling adventurous, you could write such a helper yourself, as the concept is actually quite simple.
Cheers,
Dennis
You can try the Go Formatter library that implements replacement fields surrounded by curly braces {} format strings similar to Python format.
Working code example Go Playground:
package main
import (
"fmt"
"gitlab.com/tymonx/go-formatter/formatter"
)
func main() {
formatted, err := formatter.Format("Named placeholders {file}:{line}:{function}():", formatter.Named{
"line": 3,
"function": "func1",
"file": "dir/file",
})
if err != nil {
panic(err)
}
fmt.Println(formatted)
}
Output:
Named placeholders dir/file:3:func1():
Instead of using template.New, where you have to provide a template name, you
can just instantiate a template pointer:
package main
import (
"strings"
"text/template"
)
func format(s string, v interface{}) string {
t, b := new(template.Template), new(strings.Builder)
template.Must(t.Parse(s)).Execute(b, v)
return b.String()
}
func main() {
params := struct{File, Error string}{"abc", "def"}
println(format("File {{.File}} has error {{.Error}}", params))
}
Use os.Expand to replace fields in a format string. Expand replaces ${var} or $var in the string using a func(string) string mapping function.
Here are a couple of ways to wrap os.Expand in convenient to use functions:
func expandMap(s string, m map[string]string) string {
return os.Expand(s, func(k string) string { return m[k] })
}
func expandArgs(s string, kvs ...string) string {
return os.Expand(s, func(k string) string {
for i := 1; i < len(kvs); i++ {
if kvs[i-1] == k {
return kvs[i]
}
}
return ""
})
}
Example use:
s = expandMap("File ${file} had error ${error}",
map[string]string{"file": "myfile.txt", "error": "Not found"})
s = expandArgs("File ${file} had error ${error}",
"file", "myfile.txt", "error", "Not found"))
Run the code on the playground.
You can get quite close to that sweet python formatting experience:
message := FormatString("File {file} had error {error}", Items{"file"=myfile, "error"=err})
Declare the following somewhere in your code:
type Items map[string]interface{}
func FormatString(template string, items Items) string {
for key, value := range items {
template = strings.ReplaceAll(template, fmt.Sprintf("{%v}", key), fmt.Sprintf("%v", value))
}
return template
}
💡 note that my implementation is very naive and inefficient for high-performance needs
sudo make me a package
Seeing the development experience potential with having a simple signature like this, I've got tempted and uploaded a go package called format.
package main
import (
"fmt"
"github.com/jossef/format"
)
func main() {
formattedString := format.String(`hello "{name}". is lizard? {isLizard}`, format.Items{"name": "Mr Dude", "isLizard": false})
fmt.Println(formattedString)
}
https://repl.it/#jossef/format
text/template is interesting. I Provide some example below
Usage
func TestFString(t *testing.T) {
// Example 1
fs := &FString{}
fs.MustCompile(`Name: {{.Name}} Msg: {{.Msg}}`, nil)
fs.MustRender(map[string]interface{}{
"Name": "Carson",
"Msg": 123,
})
assert.Equal(t, "Name: Carson Msg: 123", fs.Data)
fs.Clear()
// Example 2 (with FuncMap)
funcMap := template.FuncMap{
"largest": func(slice []float32) float32 {
if len(slice) == 0 {
panic(errors.New("empty slice"))
}
max := slice[0]
for _, val := range slice[1:] {
if val > max {
max = val
}
}
return max
},
"sayHello": func() string {
return "Hello"
},
}
fs.MustCompile("{{- if gt .Age 80 -}} Old {{else}} Young {{- end -}}"+ // "-" is for remove empty space
"{{ sayHello }} {{largest .Numbers}}", // Use the function which you created.
funcMap)
fs.MustRender(Context{
"Age": 90,
"Numbers": []float32{3, 9, 13.9, 2.1, 7},
})
assert.Equal(t, "Old Hello 13.9", fs.Data)
}
Lib
package utils
import (
"text/template"
)
type Context map[string]interface{}
type FString struct {
Data string
template *template.Template
}
func (fs *FString) MustCompile(expr string, funcMap template.FuncMap) {
fs.template = template.Must(template.New("f-string").
Funcs(funcMap).
Parse(expr))
}
func (fs *FString) Write(b []byte) (n int, err error) {
fs.Data += string(b)
return len(b), nil
}
func (fs *FString) Render(context map[string]interface{}) error {
if err := fs.template.Execute(fs, context); err != nil {
return err
}
return nil
}
func (fs *FString) MustRender(context Context) {
if err := fs.Render(context); err != nil {
panic(err)
}
}
func (fs *FString) Clear() string {
// return the data and clear it
out := fs.Data
fs.Data = ""
return out
}
important document
https://golang.org/pkg/text/template/#hdr-Actions
Here is a function I wrote which replaces fields with strings in a map, similar to what you can do with Python. It takes a string which should have fields that look like ${field} and replaces them with any such keys in the given map like map['field']='value':
func replaceMap(s string,m *map[string]string) string {
r := regexp.MustCompile("\\${[^}]*}")
for x,i := range *m {
s = strings.Replace(s,"${"+x+"}",i,-1)
}
// Remove missing parameters
s = r.ReplaceAllString(s,"")
return s
}
Playground example:
https://go.dev/play/p/S5rF5KLooWq
I have a struct as this:
type Int64Slice []int64
type DataWrapper struct {
ListId Int64Slice `json:"listid" required`
Domain string `json:"domain" required`
Name string `json:"name,omitempty"`
}
And I need it become:
{
"listid": "1 2 3 4 5",
"domain": "mydomain"
}
I have wrote custom MarshalJSON:
func (u Int64Slice) MarshalJSON() ([]byte, error) {
var result string
if u == nil {
result = "null"
} else {
result = strings.Trim(strings.Join(strings.Fields(fmt.Sprint(u)), " "), "[]")
Logger.Debugln(result)
}
return []byte(result), nil
}
func (d *DataWrapper) ToJSON() []byte {
result, err := json.Marshal(d)
if err != nil {
log.Fatalln(err)
panic(err)
}
return result
}
At the line Logger.Debugln(result), it prints this result:
20170830090317506 20170830090026319 20170830111023194 201708301043081 ...
json: error calling MarshalJSON for type models.Int64Slice: invalid
character '2' after top-level value
I think you have it backwards.
Use the bytes.Buffer type to incrementally build up the string representation of your data.
The program
package main
import (
"bytes"
"encoding/json"
"os"
"strconv"
)
type Int64Slice []int64
func (s Int64Slice) MarshalJSON() ([]byte, error) {
if s == nil {
return []byte("null"), nil
}
var b bytes.Buffer
b.WriteByte('"')
for i, v := range s {
if i > 0 {
b.WriteByte('\x20')
}
b.WriteString(strconv.FormatInt(v, 10))
}
b.WriteByte('"')
return b.Bytes(), nil
}
func main() {
var (
a Int64Slice = nil
b = Int64Slice{
42,
12,
0,
}
)
enc := json.NewEncoder(os.Stdout)
enc.Encode(a)
enc.Encode(b)
}
Prints:
null
"42 12 0"
Playground link.
20170830090317506 20170830090026319 20170830111023194 201708301043081 is not a valid JSON value. It is interpreted as a valid number (20170830090317506) followed by a valid space, followed by invalid data, beginning with the 2 character; thus the error you observed.
It needs quotes around it:
Try something like:
result = `"` + strings.Trim(strings.Join(strings.Fields(fmt.Sprint(u)), " "), "[]") + `"`
Hi I have two problems in the following Go Program .
1. I couldn't read the space seperated string using Scanf or Scanln.
So I have added a formatted string "%q" to read space seperated string using double quotes.
Is there an alternative to read string with spaces ?
package main
import
(
"fmt"
"strings"
)
type details struct{
DataType string
Table string
}
func main(){
dt := details{}
fmt.Println("Enter the DataType")
fmt.Scanf("%q" ,&dt.DataType )
for strings.TrimSpace(dt.DataType) == "" {
fmt.Println("Enter the DataType")
fmt.Scanln(&dt.DataType)
}
//fmt.Println(dt.DataType)
fmt.Println("Enter the Table")
fmt.Scanln(&dt.Table)
for strings.TrimSpace(dt.Table) == "" {
fmt.Println("Enter a valid Table name ")
fmt.Scanln(&dt.Table)
}
}
The Console output is as follows ,
VenKats-MacBook-Air:ColumnCreator venkat$ go run test.go
Enter the DataType
"rid bigint not null"
Enter the Table
Enter a valid Table name
The Second problem is why does the control flow went to the second for loop without waiting for the user input . Does the Scanf with "%q" returned a carraige return .
Any help would be greatly appreciated
Perhaps something like this..
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
type details struct {
DataType string
Table string
}
func main() {
dt := details{}
cin := bufio.NewReader(os.Stdin)
for {
fmt.Println("Enter the DataType")
text, err := cin.ReadString('\n') // reads entire string up until the /n which is the newline deliminator
if strings.TrimSpace(text) == "" { // check to see if the input is empty
continue
}
if err == nil { // if the input is not empty then the control got this far and now we just have to check for error, assign the data, and break out of the loop .. repeat for the second input. If this is going to be something you do alot refactor the input section.
dt.DataType = text
break
} else {
fmt.Printf("An error as occured: %s\n", err.Error())
}
}
for {
fmt.Println("Enter the Table")
text, err := cin.ReadString('\n')
if strings.TrimSpace(text) == "" {
continue
}
if err == nil {
dt.Table = text
break
} else {
fmt.Printf("An error as occured: %s\n", err.Error())
}
}
fmt.Printf("%+v\n", dt)
return
}
Example of refactored code:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
type details struct {
DataType string
Table string
}
func getInput(message string, reader bufio.Reader) (input string) {
for {
fmt.Println(message)
input, err := reader.ReadString('\n')
if strings.TrimSpace(input) == "" {
continue
}
if err == nil {
break
} else {
fmt.Printf("An error as occured: %s\n", err.Error())
}
}
return
}
func main() {
dt := details{}
cin := bufio.NewReader(os.Stdin)
t := getInput("Enter the DataType", *cin)
dt.DataType = t
t = getInput("Enter the Table", *cin)
dt.Table = t
fmt.Printf("Seeing what my data looks like %+v\n", dt)
return
}