Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Idiomatic Go - Conventions over Configurations

Idiomatic Go - Conventions over Configurations

My GoKonf 2024 talk

Uğur Özyılmazel

February 18, 2024
Tweet

More Decks by Uğur Özyılmazel

Other Decks in Programming

Transcript

  1. Uğur “vigo ” Özyılmazel > [ Oldsk00L C64 / AmigA

    Scener since 1990 ] < Idiomatic Go Conventions over Configurations
  2. Sandi Metz Author of Practical Object-Oriented Design in Ruby I

    don’t know what programming language you know, I don’t know how good of a programmer you are, but there’s one thing I do know: the code you write will change.
  3. - package - variable - struct - function - method

    - interface Naming Conventions
  4. Naming Conventions package - The name should describe the purpose

    of the package. - It should consist only of letters, short and clear. - It should reflect specific, well-defined responsibilities. - Must sense what package contains.
  5. Naming Conventions package - Don ’ t steal good names

    from the user - Avoid repetition (not http.HTTPServer) - Familiar abbreviation hurts nobody (strconv, fmt) - utils, helpers, common are bad names, no sense of what the package contains. - Imagine how you will import and call the function.
  6. Naming Conventions variable The variable name should describe the value

    it holds, not the type of the variable! var companiesMap = map[string]Company var companies = map[string]Company
  7. Naming Conventions - Plural names for collections (map, slice, array)

    - i, j and k are good for for-loops - n for count, sum / amount, s for strings - k and v are good for maps - a, b, c can be used for comparing same types - x, y for local variable scope. variable
  8. Naming Conventions function Functions should be named according to the

    result they return! func Add(a, b) int {} // describes only the operation func Sum(a, b) int {} // describes the result, not the operation
  9. Naming Conventions method Methods should be named to describe the

    action they perform, opposite of function naming! func (u *User) resetPassword() error {} // describes the operation
  10. Naming Conventions method Go doesn't provide automatic support for getters

    and setters. type User struct { email string } // Email is a getter for user’s email. func (u user) Email() string { return u.email }
  11. Naming Conventions method Go doesn't provide automatic support for getters

    and setters. type User struct { email string } // SetEmail is a setter for user’s email. func (u *user) SetEmail(email string) { u.email = email }
  12. Naming Conventions interface An interface defines the behaviors! takes the

    suffix “-er ” type Stringer interface { String() string } type Storer interface { Store() error }
  13. Naming Conventions interface type ReadWriteCloser interface { Reader Writer Closer

    } An interface defines the behaviors! takes the suffix “-er ”
  14. Naming Conventions struct The importer of a package will use

    the name to refer to its contents, so exported names in the package can use that fact to avoid stutter http.HTTPRequest http.Request
  15. In any dilemma or any doubts, Go's source code is

    your friend. $(go env GOROOT)/src
  16. Don't use config structs, use only values you need -

    Complexity with defaults - Immutability and validation - Evolution over time
  17. func initServer(s *Server) error { if s.Address == "" {

    return fmt.Errorf("address field is required") } switch { case s.Port == 0: return fmt.Errorf("port field is required") case s.Port > 9999: return fmt.Errorf("invalid port number") } fmt.Printf("setting server address: %s:%d\n", s.Address, s.Port) if s.TLSEnabled { fmt.Println("TLS enabled") } return nil } type Server struct { Address string Port int TLSEnabled bool }
  18. type ServerOption func(*server) func WithAddress(address string) ServerOption { return func(c

    *server) { c.address = address } } func WithPort(port int) ServerOption { return func(c *server) { c.port = port } } func WithTLSEnabled(tlsEnabled bool) ServerOption { return func(c *server) { c.tlsEnabled = tlsEnabled } }
  19. func New(opts ...ServerOption) (Server, error) { // set defaults srvr

    := &server{ address: "0.0.0.0", port: 8000, } for _, opt := range opts { opt(srvr) } switch { case srvr.port == 0: return nil, fmt.Errorf("port field is required") case srvr.port > 9999: return nil, fmt.Errorf("invalid port number") } return srvr, nil }
  20. func main() { // server, err := New() // server,

    err := New( // WithAddress("127.0.0.1"), // ) // server, err := New( // WithTLSEnabled(true), // ) server, err := New( WithAddress("127.0.0.1"), WithPort(9001), WithTLSEnabled(true), ) if err != nil { log.Fatal(err) } _ = server.Serve() }
  21. Use typed arguments! func demoClient(apiKey, apiSecret string) error { if

    err := demoClient("myKey", "mySecret"); err != nil { if err := demoClient("mySecret", "myKey"); err != nil {
  22. Use typed arguments! package main import "fmt" type apiKey string

    // type definition type myApiKey = apiKey // type alias func inspectString(s apiKey) { fmt.Println("s is", s) } func main() { var a apiKey = "api-key" var b myApiKey = "my-api-key" inspectString(a) // apiKey type inspectString(b) // myApiKey type inspectString("c") // string type } // s is api-key // s is my-api-key // s is c
  23. type MyClientApiKey struct { Value string } func (f MyClientApiKey)

    String() string { return f.Value } type MyClientApiSecret struct { Value string } func (f MyClientApiSecret) String() string { return f.Value } Use typed arguments!
  24. type MyClientApiKey struct { Value string } func (f MyClientApiKey)

    String() string { return f.Value } type MyClientApiSecret struct { Value string } func (f MyClientApiSecret) String() string { return f.Value } func myClient(apiKey MyClientApiKey, apiSecret MyClientApiSecret) error { Use typed arguments!
  25. context usage - Use context, it ’ s your friend!

    - Always pass as first argument to a function - Never put context into a struct field!
  26. Make the zero value useful type Counter struct { count

    int } func (c *Counter) Increment() { c.count++ } func (c *Counter) Value() int { return c.count } var c Counter c.Increment() fmt.Println(c.Value()) // 1
  27. Keep in mind - Always early exit / early return

    from a function - Avoid init() as much as possible! - Instead of implementing custom concurrent safe map, use sync.Map - Use string concatenation instead of fmt.Print family - Use functions as types, especially in test cases *
  28. Function Type in Test Case type Case struct { Desc

    string Method string Endpoint string Payload any Body io.Reader Headers map[string]string ServiceMockers func() ExpectedStatusCode int ExpectedResponseBody string ExpectedResponseBodyContains string ExpectedResponseBodyContainsMany []string Cleanup func() }
  29. Function Type in Test Case { Desc: "interal server error

    create", Method: http.MethodPost, Payload: map[string]any{ "calling_code": "+90", "alpha2_code": "TR", }, Endpoint: baseURL, ServiceMockers: func() { s.rinkuserService.EXPECT(). CreateRinkUser(gomock.Any(), gomock.Any()). Return(nil, nil, rinkerror.ErrUnknown) }, ExpectedStatusCode: http.StatusInternalServerError, ExpectedResponseBodyContainsMany: []string{ `unknown error occurred`, }, },
  30. Organize your code as if you were going to share

    it as open-source. - README.md - structured - Code of Conduct - License - Examples - Tests