Survive under the Crap Go Error System
===
<!-- .slide: data-background-color="pink" -->
<!-- .slide: data-transition="zoom" -->
> [name=郭學聰 Hsueh-Tsung Kuo]
> [time=Sat, 14 Nov 2020] [color=red]
###### CC BY-SA 4.0
---
<!-- .slide: data-transition="convex" -->
## Who am I?
![fieliapm](https://www.gravatar.com/avatar/2aef78f04240a6ac9ccd473ba1cbd1e3?size=2048 =384x384)
<small>Someone (who?) said:
a game programmer should be able to draw cute anime character(?)</small>
----
<!-- .slide: data-transition="convex" -->
* A programmer from game company in Taiwan
* Backend (and temporary frontend) engineer
* Usually develop something related to my work in Python, Ruby, ECMAScript, Golang, C#
* ECMAScript hater since **Netscape** is dead
* Built CDN-aware game asset update system
* Professional small vehicle driver
* Draw cute anime character in spare time
---
<!-- .slide: data-transition="convex" -->
## Outline
----
<!-- .slide: data-transition="convex" -->
4. Error Usage
5. Error Design Flaw
* Error 5 Ws
* Go Error System
6. Package
* errors
* golang.org/x/xerrors
* github.com/pkg/errors
* Integration
----
<!-- .slide: data-transition="convex" -->
7. Advice
* Wrapping
* Inspection
* Displaying
* Sum Up
8. One More Thing
9. Conclusion
10. Reference
11. Q&A
---
<!-- .slide: data-transition="convex" -->
## Error Usage
----
<!-- .slide: data-transition="convex" -->
### Typical Example
```go=
func MyFunc(param1 int, param2 string) (string, error) {
// do something...
f, err := os.Open("filename.ext")
if err != nil {
return "", err
}
// do something...
return result, nil
}
```
---
<!-- .slide: data-transition="convex" -->
## Error Design Flaw
----
<!-- .slide: data-transition="convex" -->
### Error 5 Ws
* What
* Error title
* Why
* Error description
* Which
* Error type
* When
* Line number
* Where
* Call stack
----
<!-- .slide: data-transition="convex" -->
#### Who Need to Know What
* End-User
* What
* Why
* Package Importer
* Which
* Package Developer & Maintainer
* When
* Where
----
<!-- .slide: data-transition="convex" -->
### :thumbsdown: Go Error System
* [x] Error title
* [x] Error description
* [ ] Error type
* [ ] Line number
* [ ] Call stack
==Why?== <!-- .element: class="fragment" data-fragment-index="1" -->
----
<!-- .slide: data-transition="convex" -->
#### Review Typical Example
```go=
func MyFunc(param1 int, param2 string) (string, error) {
// do something...
f, err := os.Open("filename.ext")
if err != nil {
return "", err
}
// do something...
return result, nil
}
```
----
<!-- .slide: data-transition="convex" -->
#### Pitfalls
```go=
ErrSomething = errors.New("error description")
return ErrSomething
err := fmt.Errorf("error %s", detail)
return err
```
----
<!-- .slide: data-transition="convex" -->
#### Darkness
* How to classify built-in errors before Go 1.13
* Unreliable error message
* Useless error type (in most cases)
* Nothing further :face_palm:
----
<!-- .slide: data-transition="convex" -->
#### Dawn?
* 3rd party error module
* Go 1.13 error system
---
<!-- .slide: data-transition="convex" -->
## Package
----
<!-- .slide: data-transition="convex" -->
### errors
* :no_entry_sign: Go 1.13 error system
----
<!-- .slide: data-transition="convex" -->
#### Wrap & Unwrap
```go=
// wrap error
err = fmt.Errorf("... %w ...", ..., cause, ...)
// unwrap and return NEXT error, or nil
cause = errors.Unwrap(err)
```
----
<!-- .slide: data-transition="convex" -->
#### Inspect Error Chain
```go=
errors.Is(err, ErrorInstance) // must be same address
var perr *ErrorType
errors.As(err, &perr)
```
----
<!-- .slide: data-transition="convex" -->
#### Example
```go=
package main
import (
"errors"
"fmt"
)
type MyError struct {
msg string
err error
}
func NewMyError(msg string, err error) *MyError {
if err != nil {
msg = fmt.Sprintf("%s: %s", msg, err.Error())
}
return &MyError{
msg: msg,
err: err,
}
}
func (e *MyError) Error() string {
return e.msg
}
func (e *MyError) Unwrap() error {
return e.err
}
func main() {
var err error
err = errors.New("err1")
err = fmt.Errorf("err2: %w", err)
myErr := NewMyError("my err3", err)
myErr2 := NewMyError("my err3", err)
err = myErr
err = fmt.Errorf("err4: %w", err)
err = fmt.Errorf("err5: %w", err)
if err != nil {
fmt.Println("error is:", err)
if errors.Is(err, myErr) {
fmt.Println("it is myErr!")
}
if !errors.Is(err, myErr2) {
fmt.Println("it is NOT myErr2!")
}
fmt.Println("")
var myError *MyError
if errors.As(err, &myError) {
fmt.Println("failed:", err.Error())
fmt.Println("MyError:", myError.Error())
}
}
}
```
----
<!-- .slide: data-transition="convex" -->
#### Result
```shell
error is: err5: err4: my err3: err2: err1
it is myErr!
it is NOT myErr2!
failed: err5: err4: my err3: err2: err1
MyError: my err3: err2: err1
```
----
<!-- .slide: data-transition="convex" -->
### golang.org/x/xerrors
* :no_entry_sign: golang.org/x/xerrors by Go team members
* https://github.com/golang/xerrors
* https://godoc.org/golang.org/x/xerrors
----
<!-- .slide: data-transition="convex" -->
#### Additional Features
* Frame
* New & Wrap
* Attach current function & line number
* Inspect
* Print current function & line number
----
<!-- .slide: data-transition="convex" -->
#### Usage
```go=
// new error
err = xerrors.New("...")
// wrap error
err = xerrors.Errorf("... %w ...", ..., cause, ...)
// print error chain and frames
fmt.Printf("%+v\n", err)
```
----
<!-- .slide: data-transition="convex" -->
#### Example
```go=
package main
import (
"fmt"
"golang.org/x/xerrors"
)
func MyFuncInner() error {
return xerrors.New("inner error")
}
func MyFuncMiddle() error {
err := MyFuncInner()
return xerrors.Errorf("middle error: %w", err)
}
func MyFuncOuter() error {
err := MyFuncMiddle()
return xerrors.Errorf("outer error: %w", err)
}
func main() {
err := MyFuncOuter()
fmt.Printf("%v\n\n", err)
fmt.Printf("%+v\n", err)
}
```
----
<!-- .slide: data-transition="convex" -->
#### Result
```shell
outer error: middle error: inner error
outer error:
main.MyFuncOuter
/.../go/src/survive_under_the_crap_go_error_system/xerrors_example.go:20
- middle error:
main.MyFuncMiddle
/.../go/src/survive_under_the_crap_go_error_system/xerrors_example.go:15
- inner error:
main.MyFuncInner
/.../go/src/survive_under_the_crap_go_error_system/xerrors_example.go:10
```
----
<!-- .slide: data-transition="convex" -->
### github.com/pkg/errors
* :thumbsup: github.com/pkg/errors by Dave Cheney
* https://github.com/pkg/errors
* https://godoc.org/github.com/pkg/errors
----
<!-- .slide: data-transition="convex" -->
#### Wrap & Unwrap
```go=
// new error
err = pkg_errors.New("...")
// wrap error
err = pkg_errors.Wrap(cause, "...")
err = pkg_errors.Wrapf(cause, "oh noes #%d", 2)
// unwrap the whole error chain and return the MOST INNER error, or return the error itself
cause = pkg_errors.Cause(err)
```
----
<!-- .slide: data-transition="convex" -->
#### Inspect Error Chain
```go=
fmt.Printf("%+v\n", err)
```
----
<!-- .slide: data-transition="convex" -->
#### Example
```go=
package main
import (
"fmt"
pkg_errors "github.com/pkg/errors"
)
func MyFuncInner() error {
return pkg_errors.New("inner error")
}
func MyFuncMiddle() error {
err := MyFuncInner()
return pkg_errors.Wrap(err, "middle error")
}
func MyFuncOuter() error {
err := MyFuncMiddle()
return pkg_errors.Wrapf(err, "outer error - %d", 2)
}
func main() {
err := MyFuncOuter()
fmt.Printf("%v\n\n", err)
fmt.Printf("%+v\n", err)
}
```
----
<!-- .slide: data-transition="convex" -->
#### Result
```shell
outer error - 2: middle error: inner error
inner error
main.MyFuncInner
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:10
main.MyFuncMiddle
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:14
main.MyFuncOuter
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:19
main.main
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:24
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374
middle error
main.MyFuncMiddle
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:15
main.MyFuncOuter
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:19
main.main
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:24
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374
outer error - 2
main.MyFuncOuter
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:20
main.main
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:24
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374
```
----
<!-- .slide: data-transition="convex" -->
### Integration
* for http server
* use the gin, luke
* <small>https://github.com/gin-gonic/gin#model-binding-and-validation</small>
```go=
// push error
c.Error(err)
// list error
c.Errors.ByType(errType)
```
---
<!-- .slide: data-transition="convex" -->
## Advice
----
<!-- .slide: data-transition="convex" -->
### Wrapping
Wrap as early as possible
```go=
import (
"errors"
"fmt"
"golang.org/x/xerrors"
pkg_errors "github.com/pkg/errors"
)
func MyFunc(param1 int, param2 string) (string, error) {
// do something...
r, err := foreign_package.ForeignFunc()
if err != nil {
err = fmt.Errorf("<error description>: %w", err)
err = xerrors.Errorf("<error description>: %w", err)
err = pkg_errors.Wrap(err, "<error description>")
return "", err
}
// do something...
return result, nil
}
```
----
<!-- .slide: data-transition="convex" -->
### Inspection
Collect and log error at entry function or middleware
```go=
// for Go 1.13 error system
func ExtractErrorMessageChain(err error) string {
var b strings.Builder
for e := err; e != nil; e = errors.Unwrap(e) {
b.WriteString(e.Error())
b.WriteString("\n")
}
return b.String()
}
func MyEntryFunc() {
// do something...
r, err := MyFunc(param1, param2)
// for Go 1.13 error system
fmt.Printf("%s", ExtractErrorMessageChain(err))
// for golang.org/x/xerrors & github.com/pkg/errors
fmt.Printf("%+v\n", err)
}
```
----
<!-- .slide: data-transition="convex" -->
### Displaying
Only show the most outer error message to end-user
```go=
// for Go 1.13 error system & golang.org/x/xerrors
func WrapError(msg string, err error) error {
return fmt.Errorf("%s: %w", msg, err)
}
// for all error frameworks
func GetErrorMessageTitle(err error) string {
return strings.SplitN(err.Error(), ":", 2)[0]
}
```
----
<!-- .slide: data-transition="convex" -->
### Sum Up
----
<!-- .slide: data-transition="convex" -->
#### Example
* Go 1.13 error system
* Go standard HTTP library
* github.com/julienschmidt/httprouter
```go=
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
//"reflect"
"github.com/julienschmidt/httprouter"
)
// utility function
// for Go 1.13 error system
func ExtractErrorMessageChain(err error) string {
var b strings.Builder
for e := err; e != nil; e = errors.Unwrap(e) {
b.WriteString(e.Error())
b.WriteString("\n")
}
return b.String()
}
// for Go 1.13 error system & golang.org/x/xerrors
func WrapError(msg string, err error) error {
return fmt.Errorf("%s: %w", msg, err)
}
// for all error frameworks
func GetErrorMessageTitle(err error) string {
return strings.SplitN(err.Error(), ":", 2)[0]
}
// compare error using first part of message before colon, for example: "<first>: <rest>"
func IsErrorEqual(e1 error, e2 error) bool {
if e1 == nil || e2 == nil {
return e1 == e2
} else {
// CAUTION: error type is useless (in most cases)
// return reflect.TypeOf(e1) == reflect.TypeOf(e2) && GetErrorMessageTitle(e1) == GetErrorMessageTitle(e2)
return GetErrorMessageTitle(e1) == GetErrorMessageTitle(e2)
}
}
// ApiError
type ApiError struct {
statusCode int
msg string
err error
}
func NewApiError(statusCode int, msg string, err error) error {
if err != nil {
msg = fmt.Sprintf("%s: %s", msg, err.Error())
}
return &ApiError{statusCode: statusCode, msg: msg, err: err}
}
func (e *ApiError) Error() string {
return e.msg
}
func (e *ApiError) Unwrap() error {
return e.err
}
func (e *ApiError) StatusCode() int {
return e.statusCode
}
func WrapApiError(template error, err error) error {
apiErrorTemplate, ok := template.(*ApiError)
if ok {
err = NewApiError(apiErrorTemplate.StatusCode(), apiErrorTemplate.Error(), err)
} else {
err = WrapError(template.Error(), err)
}
return err
}
// handler
type HandleE func(http.ResponseWriter, *http.Request, httprouter.Params) error
func ErrAwareHandle(h HandleE) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
err := h(w, r, ps)
if err != nil {
// respond error with simplified message
errorMessage := GetErrorMessageTitle(err)
var statusCode int
switch e := err.(type) {
case *ApiError:
statusCode = e.StatusCode()
default:
statusCode = http.StatusInternalServerError
}
http.Error(w, errorMessage, statusCode)
// log detailed error
messageChain := ExtractErrorMessageChain(err)
fmt.Fprintf(os.Stderr, "error:\n%s====\n", messageChain)
}
}
}
// usage
var (
ErrEmptyMessage = errors.New("message is empty")
ErrDirtyMessage = errors.New("助兵衛")
ErrHentaiMessage = errors.New("変態")
)
func detectNonDirtyMessage(msg string) error {
if msg == "FQ" {
return ErrDirtyMessage
}
return nil
}
func processMessage(msg string) (string, error) {
if msg == "" {
return "", ErrEmptyMessage
}
err := detectNonDirtyMessage(msg)
if err != nil {
return "", WrapApiError(ErrHentaiMessage, err)
}
return msg, nil
}
var (
ErrMessageLost = NewApiError(http.StatusBadRequest, "message lost", nil)
ErrYouDirty = NewApiError(http.StatusForbidden, "you dirty", nil)
)
type Input struct {
Msg string `json:msg`
}
func example(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error {
name := ps.ByName("name")
if name != "say" {
err := errors.New("must be /error_test/say")
return NewApiError(http.StatusNotFound, "not found", err)
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
// this statement won't wrap error, it just concatenates error message
return fmt.Errorf("body read failed: %v", err)
}
if len(body) < 2 {
return errors.New("body is not enough")
}
var input Input
if err = json.Unmarshal(body, &input); err != nil {
return NewApiError(http.StatusBadRequest, "bad request", err)
}
resp, err := processMessage(input.Msg)
if IsErrorEqual(err, ErrEmptyMessage) {
return WrapApiError(ErrMessageLost, err)
}
if IsErrorEqual(err, ErrHentaiMessage) {
return WrapApiError(ErrYouDirty, err)
}
if err != nil {
return err
}
w.WriteHeader(200)
w.Write([]byte("you sent:\n"))
w.Write([]byte(resp))
w.Write([]byte("\n"))
return nil
}
func main() {
router := httprouter.New()
router.POST("/error_test/:name", ErrAwareHandle(example))
err := http.ListenAndServe(":8000", router)
if err != nil {
panic(err)
}
}
// client example
// curl -v -X POST --data '{"msg": "FQ2"}' http://localhost:8000/error_test/say
```
----
<!-- .slide: data-transition="convex" -->
#### Integrate with Error Type from Foreign Packages
* If we use internal error types from
* golang.org/x/xerrors
* github.com/pkg/errors
* We could store error message like below
```go=
"403|forbidden: <error description>: <error description>: ..."
```
<!-- .element: class="fragment" data-fragment-index="1" -->
---
<!-- .slide: data-transition="convex" -->
## One More Thing
----
<!-- .slide: data-transition="convex" -->
### Fail Fast
* When error occurs:
* Stop immediately
* Report the error as early as possible
----
<!-- .slide: data-transition="convex" -->
### Idiom Error Handling
```go=
func MyFunc(param1 int, param2 string) (string, error) {
// do something...
f, err := os.Open("filename.ext")
if err != nil {
return "", err
}
// do something...
n, err := io.ReadFull(f, buf1)
if err != nil {
return "", err
}
// do something...
n, err = f.Read(buf2)
if err != nil {
return "", err
}
// do something...
return result, nil
}
```
----
<!-- .slide: data-transition="convex" -->
### Error Handling in Foreign Language
![rust](https://www.rust-lang.org/static/images/rust-logo-blk.svg)
----
<!-- .slide: data-transition="convex" -->
```rust=
panic!()
Result<T, E>
Option<T>
myobj.myfn()? // ? operator
```
<small>https://doc.rust-lang.org/book/ch09-00-error-handling.html</small>
----
<!-- .slide: data-transition="convex" -->
```rust=
e.source()
e.backtrace()
```
<small>https://doc.rust-lang.org/std/error/trait.Error.html</small>
----
<!-- .slide: data-transition="convex" -->
### Evolution?
```go=
func MyFunc(param1 int, param2 string) (string, error) {
// do something...
f := try(os.Open("filename.ext"))
// do something...
n := try(io.ReadFull(f, buf1))
// do something...
n = try(f.Read(buf2))
// do something...
return result, nil
}
```
----
<!-- .slide: data-transition="convex" -->
### Impossible!?
Because something mess up:
```go=
if err != nil {
if err != io.EOF {
return "", err
} else {
return result, nil
}
}
```
----
<!-- .slide: data-transition="convex" -->
人生オワタ \(\^o\^)/ I am done!
----
<!-- .slide: data-transition="convex" -->
### Evolution Again
Add handle:
```go=
func MyFunc(param1 int, param2 string) (string, error) {
handle err {
if err != io.EOF {
return "", err
} else {
return result, nil
}
}
// do something...
f := check os.Open("filename.ext")
// do something...
n := check io.ReadFull(f, buf1)
// do something...
n = check f.Read(buf2)
// do something...
return result, nil
}
```
<small>https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md</small>
----
<!-- .slide: data-transition="convex" -->
![讓我們看下去](https://stickershop.line-scdn.net/stickershop/v1/sticker/16846578/iPhone/sticker@2x.png =531x495)
<small>https://store.line.me/stickershop/product/8601/zh-Hant</small>
---
<!-- .slide: data-transition="convex" -->
## Conclusion
----
<!-- .slide: data-transition="convex" -->
### Advice
* Wrapping
* Wrap as early as possible
* Inspection
* Collect and log error at entry function or middleware
* Displaying
* Only show the most outer error message to end-user
----
<!-- .slide: data-transition="convex" -->
### Promote
* :thumbsup: github.com/pkg/errors by Dave Cheney
* https://github.com/pkg/errors
* https://godoc.org/github.com/pkg/errors
----
<!-- .slide: data-transition="convex" -->
### Bless
:hash: {錯誤抓精光 碼農發大財|Errors get eliminated, coders get richer.}
> [name=郭學聰 Hsueh-Tsung Kuo] [time=2020_11_14] [color=red] :notebook:
---
<!-- .slide: data-transition="convex" -->
## Reference
----
<!-- .slide: data-transition="convex" -->
### Resources
* Go 2 Draft Designs
* <small>https://go.googlesource.com/proposal/+/master/design/go2draft.md</small>
* Proposal: Go 2 Error Inspection
* <small>https://go.googlesource.com/proposal/+/master/design/29934-error-values.md</small>
----
<!-- .slide: data-transition="convex" -->
### Resources
* Working with Errors in Go 1.13
* https://blog.golang.org/go1.13-errors
* golang.org/x/xerrors by Go team members
* https://github.com/golang/xerrors
* https://godoc.org/golang.org/x/xerrors
* github.com/pkg/errors by Dave Cheney
* https://github.com/pkg/errors
* https://godoc.org/github.com/pkg/errors
---
<!-- .slide: data-transition="zoom" -->
## Q&A
---
<style>
.reveal {
background: #FFDFEF;
color: black;
}
.reveal h2,
.reveal h3,
.reveal h4,
.reveal h5,
.reveal h6 {
color: black;
}
.reveal code {
font-size: 18px !important;
line-height: 1.2;
}
.progress div{
height:14px !important;
background: hotpink !important;
}
// original template
.rightpart{
float:right;
width:50%;
}
.leftpart{
margin-right: 50% !important;
height:50%;
}
.reveal section img { background:none; border:none; box-shadow:none; }
p.blo {
font-size: 50px !important;
background:#B6BDBB;
border:1px solid silver;
display:inline-block;
padding:0.5em 0.75em;
border-radius: 10px;
box-shadow: 5px 5px 5px #666;
}
p.blo1 {
background: #c7c2bb;
}
p.blo2 {
background: #b8c0c8;
}
p.blo3 {
background: #c7cedd;
}
p.bloT {
font-size: 60px !important;
background:#B6BDD3;
border:1px solid silver;
display:inline-block;
padding:0.5em 0.75em;
border-radius: 8px;
box-shadow: 1px 2px 5px #333;
}
p.bloA {
background: #B6BDE3;
}
p.bloB {
background: #E3BDB3;
}
/*.slide-number{
margin-bottom:10px !important;
width:100%;
text-align:center;
font-size:25px !important;
background-color:transparent !important;
}*/
iframe.myclass{
width:100px;
height:100px;
bottom:0;
left:0;
position:fixed;
border:none;
z-index:99999;
}
h1.raw {
color: #fff;
background-image: linear-gradient(90deg,#f35626,#feab3a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: hue 5s infinite linear;
}
@keyframes hue {
from {
filter: hue-rotate(0deg);
}
to {
filter: hue-rotate(360deg);
}
}
.progress{
height:14px !important;
}
.progress span{
height:14px !important;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAMCAIAAAAs6UAAAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QUNCQzIyREQ0QjdEMTFFMzlEMDM4Qzc3MEY0NzdGMDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QUNCQzIyREU0QjdEMTFFMzlEMDM4Qzc3MEY0NzdGMDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBQ0JDMjJEQjRCN0QxMUUzOUQwMzhDNzcwRjQ3N0YwOCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBQ0JDMjJEQzRCN0QxMUUzOUQwMzhDNzcwRjQ3N0YwOCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PovDFgYAAAAmSURBVHjaYvjPwMAAxjMZmBhA9H8INv4P4TPM/A+m04zBNECAAQBCWQv9SUQpVgAAAABJRU5ErkJggg==") repeat-x !important;
}
.progress span:after,
.progress span.nyancat{
content: "";
background: url('data:image/gif;base64,R0lGODlhIgAVAKIHAL3/9/+Zmf8zmf/MmZmZmf+Z/wAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/wtYTVAgRGF0YVhNUDw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpDMkJBNjY5RTU1NEJFMzExOUM4QUM2MDAwNDQzRERBQyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCREIzOEIzMzRCN0IxMUUzODhEQjgwOTYzMTgyNTE0QiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCREIzOEIzMjRCN0IxMUUzODhEQjgwOTYzMTgyNTE0QiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkM1QkE2NjlFNTU0QkUzMTE5QzhBQzYwMDA0NDNEREFDIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMyQkE2NjlFNTU0QkUzMTE5QzhBQzYwMDA0NDNEREFDIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8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') !important;
width: 34px !important;
height: 21px !important;
border: none !important;
float:right;
margin-top:-7px;
margin-right:-10px;
}
</style>
{"metaMigratedAt":"2023-06-15T15:10:47.958Z","metaMigratedFrom":"YAML","title":"Survive under the Crap Go Error System","breaks":true,"description":"View the slide with \"Slide Mode\".","slideOptions":"{\"spotlight\":{\"enabled\":false},\"allottedMinutes\":25}","contributors":"[{\"id\":\"ea27dcd7-a3f2-47c2-b25e-6760e7936c38\",\"add\":75912,\"del\":47990}]"}