9.9 KiB
Gotlin
Gotlin is a Kotlin-flavored language implemented in Go. It compiles typed Gotlin source to Go and uses the Go toolchain for binaries, packages, and interop.
package main
fun fibonacci(value: Int): Int {
if (value < 2) { return value }
return fibonacci(value - 1) + fibonacci(value - 2)
}
fun main() {
println(fibonacci(10))
}
Compiler pipeline
Lexer
-> parser and syntax AST
-> package resolver and structural TypeRef resolution
-> lexical symbols, type checking, effects, and diagnostics
-> typed HIR
-> Go emission
-> Go compiler
Semantic types distinguish named values, Gotlin classes, nullable values, Go
pointers, functions, generics, tuples, and imported Gotlin classes. A Gotlin
class remains User throughout semantic analysis; only Type-to-Go lowering
chooses the *User representation.
Separately compiled Gotlin packages expose a versioned .gti.json interface
containing classes, enums, signatures, generics, and inferred effects. The
compiler and LSP load package interfaces before using go/types for ordinary
Go dependencies.
Functions and values
fun greet(name: String): String {
val message = "Hello " + name
return message
}
fun doubled(value: Int) = value * 2
val is immutable and var is mutable:
val accountId = "account-1"
var attempts = 0
attempts += 1
Supported control flow includes if, else, while, for, try, catch,
throw, exhaustive match, and defer.
Classes and interfaces
Gotlin classes are reference-valued by default:
interface Greeter {
fun greet(name: String): String
}
class PrefixGreeter(val prefix: String): Greeter {
fun greet(name: String) = prefix + name
}
fun create(): PrefixGreeter = PrefixGreeter("Hello ")
Use * only for Go pointer types:
fun handle(request: *http.Request, pool: *pgxpool.Pool) { }
Imported Gotlin classes retain reference semantics across package boundaries:
fun publish(lifecycle: platform.Lifecycle) { }
Null safety
Types are non-nullable unless marked with ?:
fun email(user: User?): String {
if (user == null) { return "missing" }
return user.email
}
Safe access and non-null assertions are available:
val city: String? = user?.address?.city
val required: User = optionalUser!!
The semantic analyzer smart-casts values after null checks and guard clauses.
Data classes and JSON
@jsonNaming(camelCase)
data class AccountReply(
var accountId: String,
var availableBalance: Double
)
Supported JSON policies are snakeCase, camelCase, pascalCase, and
kebabCase.
val body = json.marshal(reply).unwrap()
val decoded = json.decode<AccountReply>(body).unwrap()
Resources can be embedded at package scope:
import embed
@embed("static/*") val assets: embed.FS
Enums and match
Enums support payloadless and payload variants:
enum PaymentResult {
Accepted(String)
Rejected(String)
Pending
}
Matches are exhaustive and may return values:
fun description(result: PaymentResult) = match (result) {
PaymentResult.Accepted(id) -> "accepted " + id
PaymentResult.Rejected(reason) -> "rejected " + reason
PaymentResult.Pending -> "pending"
}
Payloadless enums are represented as exact string-backed values, making them suitable for JSON and PostgreSQL columns.
Result error handling
fun parse(value: String): Result<Int, Error> = strconv.atoi(value)
fun doubled(value: String): Result<Int, Error> {
val parsed = strconv.atoi(value)?
return Result.Ok(parsed * 2)
}
Available operations include ?, unwrap(), unwrapOr(value), explicit
destructuring, and exhaustive Result matching. Go (T, error) and error-only
returns adapt to Result when required by context.
Generics and lambdas
data class Box<T>(var value: T) {
fun get(): T = value
}
fun identity<T>(value: T): T = value
val inferred = identity(42)
val explicit = identity<String>("value")
val boxed = Box("text")
Higher-order functions use Kotlin-style function types and trailing lambdas:
fun transform<T, R>(value: T, block: (T) -> R): R = block(value)
val answer = transform(21) { value ->
value * 2
}
The final expression is returned automatically for value lambdas. Unit lambdas
execute their final expression as a statement. Explicit return is also
supported.
Structured concurrency and context
Coroutine effects are inferred from direct and transitive calls:
fun poll() {
while (isActive()) {
receiveEvents()
delay(1000)
}
}
fun main() {
runBlocking {
launch { poll() }
}
}
launch creates a structured Unit child. async<T> creates a typed deferred
child. Scopes wait for children, propagate failures, and cancel siblings.
Available operations include runBlocking, withContext, coroutineScope,
launch, async, await, delay, withTimeout, isActive, and
coroutineContext.
HTTP request contexts can establish an ambient scope with an expression-bodied function:
fun handle(request: *http.Request) = withContext(request.context()) {
service.processRequest()
}
Go calls whose first parameter is context.Context receive the ambient context
automatically when that argument is omitted:
fun command(): *exec.Cmd = exec.commandContext("date")
Passing an explicit context suppresses automatic injection.
Collections and channels
val names = listOf<String>("Ada", "Linus")
val scores = mapOf<String, Int>("Ada", 10, "Linus", 8)
val channel = Channel<String>(1)
channel.send("ready")
val message = channel.read()
Collection types include List<T>, MutableList<T>, Map<K, V>, and
MutableMap<K, V>.
Structural mapping
mapTo maps compatible classes, enums, collections, and nullable values:
fun response(account: AccountEntity): AccountResponse = account.mapTo()
val response = account.mapTo<AccountResponse>()
Fields are matched by name and mappings are validated recursively with path-specific compile errors.
Typed PostgreSQL DSL
SQL rows are data classes with table metadata:
@table("accounts")
data class AccountRow(
@generated @id var id: String,
var customerId: String,
var balance: Double,
var closedAt: time.Time?
)
Queries, aliases, joins, groups, and aggregates
@table("customers")
data class CustomerRow(@id var id: String, var name: String)
data class AccountSummary(
var customerId: String,
var customerName: String,
var total: Double,
var entries: Long
)
fun summaries(minimum: Double): GotlinSQLQuery = sql.from<AccountRow>()
.alias("account")
.leftJoin<CustomerRow>("customer") { account, customer ->
account.customerId == customer.id
}
.select { account, customer ->
AccountSummary(
account.customerId,
customer.name,
sum(account.balance),
count()
)
}
.where { account, customer -> account.balance > minimum }
.groupBy { account, customer -> listOf(account.customerId, customer.name) }
.having { account, customer -> sum(account.balance) > minimum }
.orderByDescending { account, customer -> sum(account.balance) }
.limit(100)
.offset(20)
.build()
Join methods include join, leftJoin, and rightJoin. Aggregates include
count, countDistinct, sum, avg, min, and max.
Inserts and bulk inserts
val insert = sql.insert<AccountRow>(row).build()
val bulk = sql.insertAll<AccountRow>(rows).build()
Bulk inserts create runtime-sized PostgreSQL placeholder lists and reject empty input.
Conflicts and updates
val upsert = sql.insert<AccountRow>(row)
.onConflict { it.id }
.doUpdate { excluded ->
AccountRow.balance = excluded.balance
}
.build()
val update = sql.update<AccountRow>()
.set { row ->
row.balance = row.balance + amount
row.closedAt = now()
}
.where { it.id == accountId }
.returning { it }
.build()
Write targets are checked for existence, mutability, nullability, duplicate assignment, and value compatibility.
Execution
val accounts = sql.from<AccountRow>()
.where { it.customerId == customerId }
.fetch(pool, ctx)
.unwrap()
val account = sql.from<AccountRow>()
.where { it.id == accountId }
.single(pool, ctx)
.unwrap()
Execution terminals are fetch, single, and iterator. They return typed
Result values and use generated row scanners.
Go interop
Go packages are imported directly:
import http net.http
import pgxpool "github.com/jackc/pgx/v5/pgxpool"
Go selectors are written in lower camel case and emitted with exported Go names. Named arguments construct external Go structs:
val client = http.Client(timeout = 3 * time.second)
go/types and go/packages provide function, method, field, alias, tuple,
variadic, and error-return signatures to semantic analysis and the LSP.
Build and run
go run ./cmd/gotlinc build ./examples/hello.gt
./hello
go run ./cmd/gotlinc run ./examples/hello.gt
Emit Go source:
go run ./cmd/gotlinc build -src ./examples/hello.gt -o /tmp/hello.go
Emit and consume package interfaces:
gotlinc build -src \
-metadata-output platform.gti.json \
-metadata-package example/platform \
platform.gt -o platform.go
gotlinc build -src \
-metadata platform.gti.json \
service.gt -o service.go
Language server and editor
Build the language server:
go build -o ./bin/gotlin-lsp ./cmd/gotlin-lsp
The VS Code extension under tools/vscode-gotlin provides syntax highlighting,
snippets, diagnostics, hover, document symbols, definitions, references, and a
gopls bridge for imported Go APIs.
cd tools/vscode-gotlin
npm install
npm run check