C# Coding Style
Hand-authored C# in this repo follows Visual Studio defaults, adapted from dotnet/runtime’s conventions.
Scope. These rules govern hand-authored code: src/core/golib, src/gen/go2cs-gen,
src/core/testing, *_impl.cs companions and other [module: GoManualConversion] whole-file
hand-owns, and the utility/runner projects under src/tests and src/utilities. They do not
govern src/core/<pkg> — the converter-generated standard library — which follows the converter’s
own emission rules, not hand-authoring style.
For non-code files (XML, JSON, …), match the existing style in that file or component.
- Allman style braces: each brace begins on its own line. A single-line statement block can skip braces but must still be properly indented and not nested inside a braced block. Exception: a
usingstatement may nest directly inside anotherusingat the same indentation, even when the nestedusingcontrols a block. - Four spaces of indentation, no tabs.
m_camelCasefor private/internal instance fields,s_for static fields,t_for thread-static fields;readonlyafterstatic(static readonly, notreadonly static). Public fields are rare and use PascalCase with no prefix.- Avoid
this.unless required. - Always specify visibility, even the default (
private string m_foo, notstring m_foo); visibility is the first modifier (public abstract, notabstract public). usingimports go at the top of the file, outside anynamespaceblock, sorted alphabetically — exceptSystem.*, which sorts first.- Never more than one blank line in a row.
- No spurious spaces (
if (someVar == 0), notif ( someVar == 0 )). - If an existing hand-authored file already differs from these rules in a consistent way (e.g.
_camelCaseprivate fields instead ofm_camelCase), match that file’s style rather than mixing conventions within it. This is not license to invent a new style in a new file. - Always use actual type names; use
varonly when the type is unknown or resolved at run time. - Use language keywords, not BCL type names, for both type references and static calls:
int/string/float, notInt32/String/Single;int.Parse, notInt32.Parse. - PascalCase for constant locals and fields, except interop code whose constant must match the name/value of what it calls.
- Prefer
nameof(...)over a string literal wherever it applies. - Fields go at the top of a type declaration.
- Never hardcode a go2cs symbol glyph (
Δ,Ꮡ,ж,ᴛ, …) as a literal character or a raw\uXXXXescape. Each is defined once insrc/core/go2cs/symbols.jsonand projected intosrc/go2cs/symbols.go(Go) andsrc/core/go2cs/Symbols.cs(C#, classgo2cs.Symbols) bysrc/go2cs/internal/gensymbols; consume them viausing static go2cs.Symbols(AddressPrefix,PointerPrefix,ShadowVarMarker,TempVarMarker, …).src/check-symbol-sync.ps1fails if a projection drifts from the table. For any other non-ASCII character, use a\uXXXXescape — literal characters occasionally get garbled by a tool or editor. - Indent
#regionsections andgotolabels one level less than the surrounding code. - One type per file, named after the type — that is the default and it stays the default. Split a type across
partialfiles only when a large, mechanically shaped cluster of members (an arity ladder, a generated overload set) is crowding out the members a reader actually comes to the file to find. Name each additional file<Type>.<Cluster>.cs(builtin.GoroutineLaunchers.cs,builtin.DeferRegistrations.cs), and open it with a banner comment stating what the cluster covers, why the members take that shape, and the rule for adding to it — so the next contributor can tell at a glance whether their new member belongs there or in the primary file. The second admissible case is a utility type that is not one large thing but several independent concerns co-located under one name —TypeExtensions(an extension-method registry, Go method sets, scalar conversions) andGoReflect(naming, layout, value marshalling, field access) are the worked examples. Split those by concern rather than by shape, name each file for its concern (GoReflect.FieldAccess.cs), and hold it to the same banner requirement plus one more: say how the concern relates to the siblings, because a coupling that crosses files — a cache cleared from one file and declared in another — is invisible from either side alone. What is not admissible is splitting a type whose size is one cohesive thing; that is a design problem, not a file-layout one. The test between the two: if you cannot write each file’s banner without describing the others’ internals, the concerns are not independent and the split is hiding the real problem. - Never name a local, parameter, pattern variable or range variable
fieldinside a property accessor — including inside a local function or lambda nested in one. C# 14 makesfielda keyword in that scope (it names the property’s synthesized backing field), so the declaration is CS9273 and every use rebinds away from your variable. Every hand-authored project here setsLangVersiontolatest, so a name that compiles fine on the SDK you happen to have becomes a hard build break on the next one — that is exactly howgo2cs-genstopped building for anyone on the .NET 10 SDK (issue #34), and an older SDK will not warn you. Use a qualified name (fieldSymbol,structField); the@fieldescape compiles but reads as a workaround. Apply the same care to any future contextual keyword. This rule governs hand-authored code only — the escapes that keep generated identifiers legal (file,required,scoped, …) live in the converter’s keyword set,src/go2cs/identifierNaming.go.
src/.editorconfig enables C# auto-formatting for the projects under src/.