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