Best Cursor Rules for Go & Rust (2026)
Production-tested .cursorrules configurations for Go and Rust projects. Error handling, project structure, testing, concurrency patterns — all copy-paste ready.

Best Cursor Rules for Go & Rust (2026)
By David Henderson | March 26, 2026 | 14 min read
.cursorrules file that enforces idiomatic patterns — error handling in Go, ownership in Rust, project structure in both — transforms Cursor from a liability into a productivity multiplier. Below are my battle-tested configurations for both languages.Table of Contents
- Why Go and Rust Need Cursor Rules More Than Other Languages
- Go Rules (5 Configurations)
- Rust Rules (5 Configurations)
- Combining Rules for Your Project
- Frequently Asked Questions
Why Go and Rust Need Cursor Rules More Than Other Languages {#why-rules}
Go and Rust are opinionated languages. They have clear idioms that the community follows strictly. Code that compiles but does not follow these idioms stands out immediately in code review — and more importantly, it causes real problems in production.
Without rules, Cursor generates Go code that:
- Silently swallows errors with
_ = someFunction()instead of handling them - Uses
panicfor recoverable errors instead of returning errors - Creates deeply nested packages instead of Go's flat structure preference
- Ignores
context.Contextin functions that should propagate cancellation - Uses
interface{}(the oldany) without type assertions - Puts code in a
src/directory (Go does not usesrc/)
Without rules, Cursor generates Rust code that:
- Uses
.unwrap()everywhere instead of proper error handling with? - Clones data unnecessarily instead of borrowing
- Ignores lifetime annotations when they are needed
- Creates structs with all public fields when they should be private
- Uses
Stringwhen&stris sufficient - Ignores the module system conventions
Both languages reward discipline. Cursor Rules enforce that discipline automatically.
For more rules across every language, browse the Skiln Cursor directory.
Go Rules (5 Configurations) {#go-rules}
Go Rule 1: Foundation
The baseline for every Go project.
# Go Project Standards
This is a Go 1.22+ project. Follow these conventions for ALL Go code:
## Error Handling (CRITICAL)
- ALWAYS handle errors. NEVER use _ to discard errors.
- Return errors up the call stack: return fmt.Errorf("failed to create user: %w", err)
- Use %w for error wrapping so callers can use errors.Is() and errors.As()
- NEVER use panic() for recoverable errors — panic is only for programmer bugs
- Check errors immediately after the call that produces them
## Naming
- Use mixedCaps (camelCase), not snake_case
- Exported names start with uppercase: CreateUser, not createUser
- Interfaces named after what they do, with -er suffix: Reader, Writer, Closer
- Single-method interfaces are preferred over large interfaces
- Package names are lowercase, single word: user, not user_service
## Code Style
- Use gofmt formatting (this is non-negotiable in Go)
- Keep functions short — under 50 lines is ideal
- Return early: check error conditions first and return, keeping the happy path un-indented
- Use named return values sparingly — only when they improve readability
## Example
func GetUserByID(ctx context.Context, db *sql.DB, id int64) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("invalid user ID: %d", id)
}
var user User
err := db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id = $1", id).
Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %d not found: %w", id, ErrNotFound)
}
return nil, fmt.Errorf("failed to query user %d: %w", id, err)
}
return &user, nil
}
Go Rule 2: Project Structure
Establishes the standard Go project layout.
# Go Project Structure
Follow the standard Go project layout:
project-root/
cmd/
api/main.go — Application entrypoints (each cmd/ subfolder is a binary)
worker/main.go
internal/ — Private application code (not importable by other projects)
handler/ — HTTP handlers (thin — validate input, call service, return response)
service/ — Business logic
repository/ — Database access
model/ — Domain types and structs
middleware/ — HTTP middleware
pkg/ — Public library code (importable by other projects) — use sparingly
migrations/ — Database migrations
config/ — Configuration loading
go.mod
go.sum
## Rules
- Application code goes in internal/, NOT in the project root
- Each cmd/ subfolder contains one main.go for one binary
- Do NOT create a src/ directory — Go does not use src/
- Do NOT create deeply nested packages — prefer flat structure
- Package names match directory names exactly
- One package per directory
Go Rule 3: HTTP Handlers and Routing
For Go HTTP APIs using standard library or common routers.
# HTTP Handlers
## Rules
- Handlers are thin: validate input, call service, return response
- ALWAYS pass context.Context from the request: r.Context()
- Use proper HTTP status codes: 201 for creation, 204 for deletion, 400 for bad input
- Return JSON error responses with structured format: {"error": "message", "code": "ERROR_CODE"}
- Parse request bodies with json.NewDecoder, not ioutil.ReadAll + json.Unmarshal
- Set Content-Type header: w.Header().Set("Content-Type", "application/json")
## Example (net/http)
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := req.Validate(); err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
user, err := h.service.Create(r.Context(), req)
if err != nil {
if errors.Is(err, ErrDuplicate) {
respondError(w, http.StatusConflict, "user already exists")
return
}
respondError(w, http.StatusInternalServerError, "failed to create user")
return
}
respondJSON(w, http.StatusCreated, user)
}
Go Rule 4: Concurrency Patterns
Prevents common goroutine and channel mistakes.
# Concurrency
## Rules
- ALWAYS pass context.Context to long-running operations and respect cancellation
- NEVER start goroutines without a way to stop them (use context or done channels)
- Use sync.WaitGroup for waiting on multiple goroutines
- Use errgroup.Group (golang.org/x/sync/errgroup) for goroutines that return errors
- Prefer channels for communication, mutexes for protecting shared state
- NEVER use time.Sleep for synchronization — use proper synchronization primitives
- Close channels from the sender side only — never from the receiver
## Example (errgroup)
func ProcessItems(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // max 10 concurrent goroutines
for _, item := range items {
item := item // capture loop variable
g.Go(func() error {
return processItem(ctx, item)
})
}
return g.Wait()
}
Go Rule 5: Testing
For Go testing conventions.
# Testing
## Rules
- Test files: {file}_test.go in the SAME package
- Test functions: Test{FunctionName}_{Scenario}
- Use table-driven tests for functions with multiple cases
- Use testify/assert for assertions (not raw if statements)
- Use t.Helper() in test helper functions
- Mock interfaces, not concrete types
- Use t.Parallel() for independent tests
## Example (table-driven)
func TestGetUserByID(t *testing.T) {
tests := []struct {
name string
id int64
want *User
wantErr error
}{
{
name: "valid user",
id: 1,
want: &User{ID: 1, Name: "Alice"},
wantErr: nil,
},
{
name: "user not found",
id: 999,
want: nil,
wantErr: ErrNotFound,
},
{
name: "invalid ID",
id: -1,
want: nil,
wantErr: ErrInvalidID,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := GetUserByID(context.Background(), db, tt.id)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
Rust Rules (5 Configurations) {#rust-rules}
Rust Rule 1: Foundation
The baseline for every Rust project.
# Rust Project Standards
This is a Rust project using the 2024 edition. Follow these conventions:
## Error Handling (CRITICAL)
- NEVER use .unwrap() in production code — use ? operator or proper error handling
- .unwrap() is ONLY acceptable in tests and examples
- Use thiserror for library error types, anyhow for application error types
- Propagate errors with ?: let data = read_file(path)?;
- Use .expect("reason") over .unwrap() when you truly need to panic — the message documents why
## Ownership and Borrowing
- Prefer borrowing (&T) over ownership (T) when the function does not need to own the data
- Use &str instead of String for function parameters when possible
- NEVER clone() to fix borrow checker errors unless you have verified it is necessary
- Use Cow<str> when a function might or might not need to own the string
## Naming
- snake_case for functions, variables, modules
- PascalCase for types, traits, enums
- SCREAMING_SNAKE_CASE for constants
- Module names are singular: model, not models
## Code Style
- Run rustfmt (cargo fmt) on all code
- Zero clippy warnings: cargo clippy -- -D warnings
- Use derive macros for common traits: #[derive(Debug, Clone, PartialEq)]
- Prefer iterators and closures over explicit loops where they improve clarity
## Example
use thiserror::Error;
#[derive(Error, Debug)]
pub enum UserError {
#[error("user not found: {0}")]
NotFound(i64),
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
}
pub async fn get_user_by_id(pool: &PgPool, id: i64) -> Result<User, UserError> {
sqlx::query_as!(User, "SELECT id, name, email FROM users WHERE id = $1", id)
.fetch_optional(pool)
.await?
.ok_or(UserError::NotFound(id))
}
Rust Rule 2: Project Structure
Standard Rust project organization.
# Rust Project Structure
project-root/
src/
main.rs (or lib.rs) — Entrypoint
config.rs — Configuration
error.rs — Error types
routes/ — HTTP route handlers (if web project)
mod.rs
user.rs
services/ — Business logic
mod.rs
user.rs
models/ — Domain types
mod.rs
user.rs
db/ — Database access
mod.rs
user.rs
migrations/ — Database migrations
tests/ — Integration tests
Cargo.toml
Cargo.lock
## Rules
- Declare modules in mod.rs or parent module, not with #[path] attributes
- Keep mod.rs files clean — only pub mod declarations, no logic
- Use pub(crate) for internal visibility — not everything needs to be pub
- Integration tests go in tests/, unit tests go in the same file with #[cfg(test)]
Rust Rule 3: Async Patterns (Tokio)
For async Rust projects using Tokio.
# Async Rust (Tokio)
## Rules
- Use tokio as the async runtime — do NOT mix runtimes
- Mark async functions with async fn, not manual Future implementations
- Use tokio::spawn for concurrent tasks, tokio::select! for racing futures
- NEVER block the async runtime with synchronous operations — use tokio::task::spawn_blocking for CPU-heavy work
- Use tokio::sync::Mutex for async contexts, std::sync::Mutex for sync contexts — NEVER use std::sync::Mutex in async code
- Prefer structured concurrency: spawn tasks from a known scope and join them
## Example
use tokio::task::JoinSet;
async fn process_items(items: Vec<Item>) -> Result<Vec<Output>, Error> {
let mut set = JoinSet::new();
for item in items {
set.spawn(async move {
process_item(item).await
});
}
let mut results = Vec::new();
while let Some(result) = set.join_next().await {
results.push(result??);
}
Ok(results)
}
Rust Rule 4: Axum Web Framework
For Rust web projects using Axum.
# Axum Web Framework
## Rules
- Handlers are async functions that return impl IntoResponse
- Use axum::extract for path params, query params, JSON bodies, state
- Use Extension or State for shared application state (database pool, config)
- Return proper HTTP status codes with (StatusCode, Json(body))
- Use tower middleware for cross-cutting concerns (logging, auth, CORS)
## Example
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
Json,
};
pub async fn get_user(
State(pool): State<PgPool>,
Path(id): Path<i64>,
) -> Result<impl IntoResponse, AppError> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_optional(&pool)
.await?
.ok_or(AppError::NotFound("user"))?;
Ok(Json(user))
}
pub async fn create_user(
State(pool): State<PgPool>,
Json(input): Json<CreateUserInput>,
) -> Result<impl IntoResponse, AppError> {
let user = sqlx::query_as!(
User,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
input.name,
input.email
)
.fetch_one(&pool)
.await?;
Ok((StatusCode::CREATED, Json(user)))
}
Rust Rule 5: Testing
For Rust testing patterns.
# Testing
## Unit Tests
- Put unit tests at the bottom of the file in a #[cfg(test)] module
- Test function names: test_{function}_{scenario}
- Use assert!, assert_eq!, assert_ne!, and assert_matches!
- Test error cases, not just happy paths
## Integration Tests
- Integration tests go in tests/ directory
- Each file in tests/ is compiled as a separate crate
- Use a shared setup module in tests/common/mod.rs
## Rules
- Use .unwrap() freely in tests — it is acceptable and preferred over error handling
- Use #[should_panic(expected = "...")] for testing panics
- Use #[tokio::test] for async tests
- Use rstest for parameterized tests
- Mock external dependencies with mockall or test doubles
## Example
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_get_user_returns_user() {
let pool = setup_test_db().await;
let user = create_test_user(&pool).await;
let result = get_user_by_id(&pool, user.id).await.unwrap();
assert_eq!(result.name, "Test User");
assert_eq!(result.email, "test@example.com");
}
#[tokio::test]
async fn test_get_user_not_found() {
let pool = setup_test_db().await;
let result = get_user_by_id(&pool, 99999).await;
assert!(matches!(result, Err(UserError::NotFound(99999))));
}
}
Combining Rules for Your Project {#combining}
For a Go API Project
Combine: Rule 1 (Foundation) + Rule 2 (Structure) + Rule 3 (HTTP Handlers) + Rule 5 (Testing). Add Rule 4 (Concurrency) if your project uses goroutines heavily. This gives you roughly 800 words — well under Cursor's context limit.
For a Rust Web API (Axum + Tokio)
Combine: Rule 1 (Foundation) + Rule 2 (Structure) + Rule 3 (Async/Tokio) + Rule 4 (Axum) + Rule 5 (Testing). This is about 900 words — comprehensive but still leaves room for code context.
For a Rust CLI Tool
Combine: Rule 1 (Foundation) + Rule 2 (Structure) + Rule 5 (Testing). Skip the async and web rules. Add specific rules for clap (CLI argument parsing) and your project's output format.
For a Go Microservice
All five Go rules together, plus any framework-specific rules (gRPC, Kafka, etc.). The combined set is about 1,000 words.
For more language-specific rules and community-contributed configurations, the Skiln Cursor directory has the largest searchable collection organized by language and framework.
Frequently Asked Questions {#faq}
Can I use the same .cursorrules file for both Go and Rust in a monorepo?
Yes, but it is not ideal. Cursor reads one .cursorrules file from the project root. In a monorepo with both Go and Rust code, you would combine the relevant rules from both languages into one file. Label each section clearly (e.g., "# Go Rules" and "# Rust Rules") so Cursor applies the right conventions based on which file it is generating code for. Alternatively, open each language's directory as a separate Cursor workspace with its own rules.
Do these rules work with Cursor's free tier?
Yes. The .cursorrules file is read on all Cursor plans, including the free tier. The rules are processed as context — there is no paid feature required. The only limitation on the free tier is the number of AI generations per month, not the rules you can set.
How do I handle Go modules and Rust workspaces with Cursor Rules?
For Go modules, put the .cursorrules file at the module root (same directory as go.mod). For Rust workspaces with multiple crates, put it at the workspace root (same directory as the root Cargo.toml). In both cases, Cursor reads from the root of the opened folder.
Will these rules conflict with linters like golangci-lint or clippy?
No — they complement them. The rules tell Cursor to generate code that already passes your linters. For example, the Go foundation rule enforces error handling, which aligns with golangci-lint's errcheck linter. The Rust foundation rule enforces the ? operator and discourages .unwrap(), which aligns with clippy's unwrap_used lint. Your linters catch anything Cursor misses.
How often should I update my .cursorrules file?
Update it when your project conventions change — new library adopted, new pattern established, or when you notice Cursor repeatedly generating code that does not match your expectations. I typically update mine once a month. There is no need to update for Cursor version changes; the rules format has been stable.
