code.vegaprotocol.io/vega@v0.79.0/CODING_STYLE.md (about)

     1  # Coding style
     2  
     3  This document serves to outline the coding style standard we aim to follow throughout the codebase.
     4  
     5  ## Starting point
     6  
     7  As a starting point, we should follow [the official Golang `CodeReviewComments` document](https://github.com/golang/go/wiki/CodeReviewComments). The basics are:
     8  
     9  * names are `camelCased` or `CamelCased`, for functions, types, and constants alike.
    10  * Avoid stutter (`markets.NewMarket` should be `markets.New()` etc...)
    11  * Code should be passed though the `gofmt` tool
    12  * ...
    13  
    14  ## Superset
    15  
    16  As a basis, we're using the Golang `CodeReviewComments`. We're adding a few things to that that either have proven to be issues in our codebase, or things that are considered _good practice_, and we therefore want to enforce.
    17  
    18  1. Use directional channels whenever possible. The gRPC streams are used to subscribe to updates to specific bits of data. Internally, these subscriptions take the shape of channels. Because streams are read-only, the channels created and passed around are, by definition read-only. The return types should reflect this:
    19  
    20  ```go
    21  // SubscribeToSomething the simplest example of a subscription
    22  func (t *T) SubscribeToSomething(ctx context.Context) (<-chan *types.Something, error) {
    23      if err := t.addSubscription(); err != nil {
    24          return nil, err
    25      }
    26      ch := make(chan *types.Something)
    27      go func () {
    28          defer func() {
    29              t.rmSubscription()
    30              close(ch)
    31          }()
    32          defer close(ch)
    33          for {
    34              select {
    35              case <-ctx.Done():
    36                  return
    37              default:
    38                  for _, s := range t.latestSomethings() {
    39                      ch <- s
    40                  }
    41              }
    42          }
    43      }()
    44      return ch, nil
    45  }
    46  ```
    47  
    48  2. Unit tests cover the exported API. A single exported function performs a straightforward task (input X produces output Y). How that logic is implemented is not relevant. Whether the functionality is implemented in the function body, or spread out over 20 unexported functions is irrelevant. The implementation details should be something we can refactor, and we should be able to verify the validity of the refactor by running the tests and still have them pass. To this end, unit tests are defined in a `package_test` package.
    49  All dependencies of the tested unit are mocked using `mockgen`, and expected calls to dependencies are checked using the mocks.
    50  
    51  3. The interface any given package uses is defined inside the package itself, not in the package that implements the required interface.
    52  
    53  4. Constructor functions (`New`) return types, but accept interfaces. For example, an engine constructor may depend on a buffer, the constructor should look like this:
    54  
    55  ```go
    56  // FooBuffer ..
    57  
    58  //go:generate go run github.com/golang/mock/mockgen -destination mocks/foo_buffer_mock.go -package mocks code.vegaprotocol.io/vega/foo FooBuffer
    59  type FooBuffer interface {
    60      Add(types.Foo)
    61  }
    62  
    63  // NewFooEngine returns new foo engine, requires the foo buffer
    64  func NewFooEngine(fooBuf FooBuffer) *fooEngine {
    65      return &fooEngine{
    66          buf: fooBuf,
    67      }
    68  }
    69  ```
    70  
    71  5. Type mapping: It's a common thing to need to map one type onto another (e.g. `log.DebugLevel` mapped onto a string representation). We use protobuf types throughout, many of which contain `oneof` fields. When we assign these values, we use a `switch` statement. This denotes that the code maps a `oneof` because:
    72      a) Only one case applies
    73      b) A switch performs better. The more `if`'s are needed, the slower the mapping becomes.
    74      c) The use of `if`'s and `else`'s makes the code look more complex than it really is. `if-else` hint at more complex logic (checking if a map contains an entry, checking for errors, etc...). Type mapping is just a matter of extracting the typed data, and assigning it.
    75  
    76  Compare the following:
    77  
    78  ```go
    79  func ifMap(assignTo T, oneOf Tb) {
    80      if a := oneOf.A(); a != nil {
    81          assignTo.A = a
    82      } else if b := oneOf.B(); b != nil {
    83          assignTo.B = b
    84      } else {
    85          return ErrUnmappableType
    86      }
    87  }
    88  
    89  func switchMap(assignTo T, oneOf Tb) {
    90      switch t := oneOf.Field.(type) {
    91      case *A:
    92          assignTo.A = t
    93      case *B:
    94          assignTo.B = t
    95      default:
    96          return ErrUnmappableType
    97      }
    98  }
    99  ```
   100  
   101  The latter not only looks cleaner, it results in fewer function calls (the `if` equivalent will call all getters until a non-nil value is returned), it's easier to maintain (adding another value is a 2 line change), and clearly communicates what this function does. Instantly, anyone looking at this code can tell that there's no business logic involved.
   102  
   103  6. Return early whenever possible.
   104  
   105  ## Unit tests
   106  
   107  Whenever implementing a new feature, new unit tests will have to be written. Unit tests, by definition, should use mocks rather than actual dependencies. We generate mocks for interfaces per package with a simple `//go:generate` command:
   108  
   109  ```go
   110  //go:generate go run github.com/golang/mock/mockgen -destination mocks/some_dependency_mock.go -package mocks code.vegaprotocol.io/vega/pkg SomeDependency
   111  type SomeDependency interface {
   112      DoFoo() error
   113      DoBar(ctx context.Context, ids []string) ([]*Bar, error)
   114  }
   115  ```
   116  
   117  From this, it ought to be clear that mocks are generated per-package (including in cases where several packages depend on a single object implementing the interface they need). Mock files are written to a sub-package/directory of the package we're testing: `mocks`. Generated files have the `_mock` suffix in their name.
   118  
   119  The unit tests themselves sit next to the package files they cover, preferably with the same name + `_test` suffix (so `engine.go` tests in `engine_test.go`).
   120  The test file itself also adds the `_test` suffix to the package name, effectively running tests in a different package. This ensures we're testing only the exported API. Covering unexported functions shouldn't be a problem. If an unexported function cannot be covered through calls made to the exported API, then it's dead code and can be removed.
   121  
   122  Tests should be grouped in terms of what they cover. Each group ideally contains a simple scenario (happy path), a failure, and a couple more complex scenario's. Taking the collateral engine as an example, we see test grouping like this:
   123  
   124  
   125  ```go
   126  func TestCollateralTransfer(t *testing.T) {}
   127  
   128  func TestCollateralMarkToMarket(t *testing.T) {}
   129  
   130  func TestAddTraderToMarket(t *testing.T) {}
   131  
   132  func TestRemoveDistressed(t *testing.T) {}
   133  
   134  func TestMarginUpdateOnOrder(t *testing.T) {}
   135  
   136  func TestTokenAccounts(t *testing.T) {}
   137  
   138  func TestEnableAssets(t *testing.T) {}
   139  
   140  func TestCollateralContinuousTradingFeeTransfer(t *testing.T) {}
   141  ```
   142  
   143  In some cases it is useful to have functions that can access internal state of a type that would not be accessible from the public API. These test helper functions are not required during normal code use, only for when we are testing and want to perform extra checks around the state of the type. For example in the matching-engine, if we delete an order we can check it has gone by querying for that order. However that does not tell us if the price level has been removed, the volume at a price level has reduced or if the number of items in the expiry collection has been reduced. We can add these helper functions in a separate file named `<type>_for_test.go` and the package name will be the same as the main code for that type.
   144  
   145  ## Protobuf
   146  
   147  In addition to the Golang code review standards, we want to be consistent:
   148  
   149  * Avoid nested types as much as possible. Enums are the notable exception here.
   150  * Fields that are ID's should be named `ID` or `FooID` (ID is CAPS).
   151  * Messages used in the API use the suffix `Request` and `Response`.
   152  * API Request/Response types, and the service definitions belong in the `proto/api` directory (and the `api` package)
   153  * Message types representing a unit of data, currently used in the core (e.g. `Order`, `Market`, `Transfer`, ...) are defined in the `proto/vega.proto` file. These types are imported under the alias `types`.
   154  * Wherever possible, add validator tags to the proto definitions.
   155  
   156  ### Example
   157  
   158  ```proto
   159  message Meta {
   160      string key = 1;
   161      string value = 2;
   162  }
   163  
   164  message Something {
   165      enum Status {
   166          Disabled = 0;
   167          Enabled = 1;
   168      }
   169      string ID = 1 [(validator.field) = {string_not_empty : true }];
   170      string marketID = 2;
   171      string partyID = 3;
   172      Status status = 4;
   173      repeated Meta meta = 5;
   174  }
   175  ```
   176  
   177  This generates the following types:
   178  
   179  ```go
   180  type Something struct {
   181      ID        string
   182      MarketID  string
   183      PartyID   string
   184      Status    Status
   185      Meta      []Meta
   186  }
   187  
   188  type Meta struct {
   189      Key    string
   190      Value  string
   191  }
   192  
   193  type Something_Status int32
   194  
   195  const (
   196      Something_Disabled Something_Status = 0
   197      Something_Enabled  Something_Status = 1
   198  )
   199  ```
   200  
   201  To add an RPC call to get this _"something"_ from the system, add a call to the `trading_data` service in `proto/api/trading.proto`:
   202  
   203  ```proto
   204  service trading_data {
   205      rpc GetSomethingsByMarketID(GetSomethingsByMarketIDRequest) returns (GetSomethingsByMarketIDResponse);
   206  }
   207  
   208  message GetSomethingsByMarketIDRequest {
   209      string marketID = 1 [(validator.field) = {string_not_empty : true}];
   210  }
   211  
   212  message GetSomethingsByMarketIDResponse {
   213      repeated vega.Something something = 1;
   214  }
   215  ```
   216  
   217  ### By popular demand:
   218  
   219  Named return values are perfectly fine. They can be useful in certain scenarios (changing return values in defer functions, for example).
   220  
   221  ## Log levels
   222  
   223  We want to be consistent regarding log levels used. We use the standard levels (`DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL` and `PANIC`). 
   224  
   225  * `DEBUG`: As the name suggests, debug logs should be used to output information that is **useful to core developers** for debugging. These logs provide information useful for developing features, or fixing bugs. Because logging things like orders has a performance impact, we wrap log statements in an `if`, making sure we only call `log.Debug` if the log level is active.
   226  
   227    ```go
   228    if log.Level() == logging.Debug {
   229        log.Debug("log entry here", logging.Order(order))
   230    }
   231    ```
   232  * `INFO`: These logs should be **informative to node operators** in the sense that they indicate that the application is working as intended, and something significant has happened. Messages should be one-off (e.g. at start-up or shutdown) or occasional (e.g. market created or settled). Do not log at `INFO` level inside loops, or in a way which means that increased activity causes a proportional increase in the number of log messages (e.g. a distressed trader was closed out, a market entered/exited auction mode).
   233  * `WARN`: These logs indicate that something unusual (but expected) has happened, the node is now operating in a sub-optimal way, and the node operator could do something to fix this to remove so that the log message would not appear.
   234  * `ERROR`: These logs indicate that there was a problem with a non-instrumental subsystem (e.g. the REST HTTP server died) but the node can continue, albeit in a degraded state (e.g. gRPC and GraphQL are fine, but not the dead REST HTTP server). The node operator probably needs to take some significant action (e.g. restarting the node, augmenting node hardware).
   235  * `FATAL`: These logs indicate that the node was unable to continue. Something went terribly wrong, and this is likely due to a bug. Immediate investigation and fixing is required. `os.Exit(1)` is called, which does not run deferred functions.
   236  * `PANIC`: We have got to a state that is critically incorrect and is not fixable. Any further execution of the application could produce incorrect results and is considered dangerous. 
   237  
   238  Notable exception: A context with timeout/cancel always returns an error if the context is cancelled (whether it be due to the time limit being reached, or the context being cancelled manually). These errors specify why a context was cancelled. This information is returned by the `ctx.Err()` function, but this should *not* be logged as an error. We log this at the `DEBUG` level. When the context for a (gRPC) stream is cancelled, for example, we should either ignore the error, or log it at the `DEBUG` level.
   239  The reason we might want to log this could be: to ensure that streams are closed correctly if the client disconnects, or the stream hasn't been read in a while. This information is useful when debugging the application, but should not be spamming the logs with `ERROR` entries: this is expected behaviour after all.
   240  
   241  ## API response errors
   242  
   243  The audience for API responses is different to the audience for log messages. An API user who submits a message and receives an error response is interested in what they can do to fix their message. They are not interested in core code (e.g. stack traces, file references or line numbers) or in the node (e.g. disk full, failed to write to badger store).
   244  
   245  ## Helpful errors
   246  
   247  Errors returned from functions should be as helpful as possible, for example by including function parameters.
   248  
   249  Example:
   250  
   251  ```go
   252  func DoAllThings(ids []string) error {
   253      for _, id := range ids {
   254          err := DoSomething(id)
   255          if err != nil {
   256              return err
   257          }
   258      }
   259  }
   260  
   261  func DoSomething(id string) error {
   262      err := doSomeSub1Thing(id)
   263      if err != nil {
   264          // details from err are lost, and there is no mention of "id".
   265          return ErrFailedToDoSomeSub1Thing
   266      }
   267  
   268      err = doSomeSub2Thing(id)
   269      if err != nil {
   270          // details from err are included, but there is still no mention of "id".
   271          return fmt.Errorf("error doing some sub2 thing: %v", err)
   272      }
   273  
   274      // ...
   275  }
   276  ```
   277  
   278  The omission of the identifier `id` means that we don't know which call to `DoSomething` was the one that caused the error.
   279  
   280  ## Inappropriate wording
   281  Some of the wording that was used as a standard 10 years ago is no long considered correct for use in open source software. We should use the updated version of these naming schemes in all of our code and documentation.
   282  
   283  * Blacklist/Whitelist -> Denylist/Allowlist
   284  * Master/Slave -> Primary/Replica
   285  * Master/Develop -> Main/Develop