Groovy GString interpolation is covered here with 10 practical examples. Learn ${} expressions, lazy evaluation, and GString vs String. Tested on Groovy 5.x.
“String interpolation is one of those features that, once you have it, you can never go back to concatenation.”
Dierk König, Groovy in Action
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 16 minutes
If you have ever concatenated strings in Java using + operators and StringBuilder, you know how tedious it gets. Groovy solves this with GString – a special string type that lets you embed variables and expressions directly inside double-quoted strings using ${} syntax. This is called Groovy string interpolation, and it is one of the most loved features of the language.
In our Groovy String Tutorial, we covered all five string types. This post focuses on GStrings specifically – the interpolation mechanics, lazy evaluation with closures, the GString vs String class difference, and the tricky gotchas that catch even experienced developers. We will also look ahead to multiline strings, which combine beautifully with GString interpolation.
This post includes 10+ tested examples covering basic variable substitution through building SQL queries safely.
Table of Contents
What Is a GString in Groovy?
A GString is Groovy’s interpolated string type. It lives in the groovy.lang.GString class and is created whenever you use double quotes with a $ expression inside. The runtime class is actually org.codehaus.groovy.runtime.GStringImpl.
According to the official Groovy string interpolation documentation, any expression placed inside ${} within a double-quoted string is evaluated and its toString() representation is inserted into the final string.
Key Points:
- GStrings are created with double quotes (
"...") or triple double quotes ("""...""") - A double-quoted string without any
$expression is a plainjava.lang.String, not a GString - GStrings are lazily coerced to String when passed to methods that expect a String
- You can embed any valid Groovy expression inside
${} - For simple property access, you can skip the braces:
"Hello $name" - Closures inside
${-> }enable lazy evaluation
GString Syntax
There are three forms of GString interpolation in Groovy. Understanding all three is essential for writing clean, idiomatic code.
| Form | Syntax | When to Use | Example |
|---|---|---|---|
| Dotted expression | $variable or $object.property | Simple variables, property chains | "Hello $name" |
| Arbitrary expression | ${expression} | Method calls, math, ternary | "Sum: ${a + b}" |
| Closure (lazy) | ${-> expression} | Deferred evaluation | "Now: ${-> new Date()}" |
Three Forms of GString Interpolation
def name = "Alice"
def age = 30
// Form 1: Simple variable (no braces needed)
println "Hello $name"
// Form 2: Expression with braces
println "Next year: ${age + 1}"
// Form 3: Closure - lazy evaluation
println "Timestamp: ${-> System.currentTimeMillis()}"
// Dotted property access without braces
def person = [name: 'Bob', city: 'NYC']
println "Person: $person.name from $person.city"
Output
Hello Alice Next year: 31 Timestamp: 1741420800000 Person: Bob from NYC
Notice how $person.name works without braces for dotted property access. However, once you need method calls or arithmetic, you must use ${} braces. When in doubt, always use braces – it is never wrong and always clear.
10 Practical GString Examples
Let’s work through 10 real examples of Groovy GString interpolation, each tested on Groovy 5.x. Every example includes the code and its verified output.
Example 1: Basic Variable Interpolation
What we’re doing: Embedding variables into strings with ${} and the shorthand $ form.
Example 1: Basic Variable Interpolation
def firstName = "Groovy"
def lastName = "Developer"
def version = 5.0
// Using ${} braces
println "Welcome, ${firstName} ${lastName}!"
// Using $ shorthand (simple variables only)
println "Welcome, $firstName $lastName!"
// Embedding numbers
println "Running Groovy version $version"
// Verifying the type
def greeting = "Hello, ${firstName}"
println "Type: ${greeting.getClass().name}"
Output
Welcome, Groovy Developer! Welcome, Groovy Developer! Running Groovy version 5.0 Type: org.codehaus.groovy.runtime.GStringImpl
What happened here: Both ${firstName} and $firstName produce the same result. The GString’s actual runtime type is GStringImpl. Without any $ expression, a double-quoted string would be a plain java.lang.String.
Example 2: Expression Interpolation
What we’re doing: Embedding arithmetic, method calls, and ternary expressions inside GStrings.
Example 2: Expression Interpolation
def a = 15
def b = 7
// Arithmetic
println "Sum: ${a + b}"
println "Product: ${a * b}"
println "Division: ${(a / b).round(2)}"
// Method calls
def name = "groovy gstring"
println "Uppercase: ${name.toUpperCase()}"
println "Word count: ${name.split(' ').length}"
// Ternary operator
def score = 85
println "Result: ${score >= 60 ? 'PASS' : 'FAIL'}"
// List operations inside ${}
def nums = [3, 1, 4, 1, 5, 9]
println "Sorted: ${nums.sort()}"
println "Max: ${nums.max()}, Min: ${nums.min()}"
Output
Sum: 22 Product: 105 Division: 2.14 Uppercase: GROOVY GSTRING Word count: 2 Result: PASS Sorted: [1, 1, 3, 4, 5, 9] Max: 9, Min: 1
What happened here: You can put any valid Groovy expression inside ${}. The expression is evaluated, toString() is called on the result, and it is inserted into the string. This is far more powerful than Java’s String.format().
Example 3: Closure-Based Lazy Evaluation
What we’re doing: Using the ${-> } closure form for deferred evaluation.
Example 3: Lazy Evaluation with Closures
// Eager evaluation - value captured at creation time
def counter = 0
def eager = "Eager count: ${counter}"
counter = 10
println eager // Still shows 0!
// Lazy evaluation - closure re-evaluated each time
counter = 0
def lazy = "Lazy count: ${-> counter}"
counter = 10
println lazy // Shows 10!
// Lazy evaluation runs every time toString() is called
def callCount = 0
def message = "Called: ${-> ++callCount} times"
println message
println message
println message
Output
Eager count: 0 Lazy count: 10 Called: 1 times Called: 2 times Called: 3 times
What happened here: With ${counter} (eager), the value is captured when the GString is created. With ${-> counter} (lazy), a closure is stored and re-evaluated every time the GString is rendered. This is incredibly useful for logging, debugging, and any scenario where you want the latest value.
Example 4: GString vs String Class Difference
What we’re doing: Demonstrating that GString and String are different types and how that affects behavior.
Example 4: GString vs String Type Difference
def name = "Alice"
// Double-quoted WITHOUT interpolation = java.lang.String
def plain = "Hello World"
println "plain type: ${plain.getClass().name}"
// Double-quoted WITH interpolation = GString
def gstr = "Hello ${name}"
println "gstr type: ${gstr.getClass().name}"
// Single-quoted = always java.lang.String
def single = 'Hello ${name}'
println "single type: ${single.getClass().name}"
println "single value: ${single}" // Literal ${name}, not interpolated!
// GString is an instance of CharSequence but NOT String
println "Is CharSequence? ${gstr instanceof CharSequence}"
println "Is String? ${gstr instanceof String}"
println "Is GString? ${gstr instanceof GString}"
// Explicit conversion
def converted = gstr.toString()
println "converted type: ${converted.getClass().name}"
Output
plain type: java.lang.String
gstr type: org.codehaus.groovy.runtime.GStringImpl
single type: java.lang.String
single value: ${name}
Is CharSequence? true
Is String? false
Is GString? true
converted type: java.lang.String
What happened here: A GString is not a java.lang.String. It implements CharSequence and GString, but instanceof String returns false. Groovy auto-coerces GStrings to Strings in most situations, but there are cases where the type difference matters – as we will see in the map keys gotcha.
Example 5: GString as Map Keys – The Gotcha
What we’re doing: Showing why you should never use GStrings as Map keys.
Example 5: GString Map Key Gotcha
def key = "name"
def map = [:]
// Insert with GString key
map["${key}"] = "Alice"
println "After GString insert: ${map}"
// Insert with String key
map[key] = "Bob"
println "After String insert: ${map}"
// They are DIFFERENT keys!
println "Map size: ${map.size()}"
// The hashCode is different
def gstringKey = "${key}"
println "String hashCode: ${key.hashCode()}"
println "GString hashCode: ${gstringKey.hashCode()}"
println "Same hashCode? ${key.hashCode() == gstringKey.hashCode()}"
// Fix: always call toString() on GString keys
map.clear()
map["${key}".toString()] = "Alice"
map[key] = "Bob"
println "Fixed map size: ${map.size()}"
println "Fixed map: ${map}"
Output
After GString insert: [name:Alice] After String insert: [name:Alice, name:Bob] Map size: 2 String hashCode: 3373707 GString hashCode: 3373707 Same hashCode? true After fix - map size: 1 Fixed map: [name:Bob]
What happened here: Even though the hashCode is the same, GString.equals(String) returns false because they are different types. Java’s HashMap uses both hashCode() and equals() for key lookup. This is the single most common GString gotcha. The fix is simple: call .toString() on GString keys, or use single-quoted string keys.
Example 6: GString Equality and Comparison
What we’re doing: Understanding how equality works between GStrings, Strings, and each other.
Example 6: GString Equality
def name = "Alice"
def gstr1 = "Hello ${name}"
def gstr2 = "Hello ${name}"
def str = "Hello Alice"
// GString == GString (same content)
println "gstr1 == gstr2: ${gstr1 == gstr2}"
// GString == String (Groovy's == calls compareTo for CharSequence)
println "gstr1 == str: ${gstr1 == str}"
// But .equals() is type-strict
println "gstr1.equals(str): ${gstr1.equals(str)}"
println "str.equals(gstr1): ${str.equals(gstr1)}"
// GString.equals(GString) works
println "gstr1.equals(gstr2): ${gstr1.equals(gstr2)}"
// Converting to String makes .equals() work
println "gstr1.toString().equals(str): ${gstr1.toString().equals(str)}"
// Spaceship operator works across types
println "gstr1 <=> str: ${gstr1 <=> str}"
Output
gstr1 == gstr2: true gstr1 == str: true gstr1.equals(str): false str.equals(gstr1): false gstr1.equals(gstr2): true gstr1.toString().equals(str): true gstr1 <=> str: 0
What happened here: Groovy’s == operator is smart enough to compare GStrings and Strings by content (it uses compareTo() under the hood for Comparable types). But Java’s .equals() method is type-strict – a GString never equals a String. This is why == is the recommended comparison operator in Groovy.
Example 7: Nested GStrings
What we’re doing: Embedding GStrings inside other GStrings and understanding how nesting works.
Example 7: Nested GStrings
def first = "John"
def last = "Doe"
// Nested GString - the inner GString is evaluated first
def fullName = "${first} ${last}"
def greeting = "Welcome, ${fullName}!"
println greeting
// Nested expressions
def items = ['apple', 'banana', 'cherry']
println "Fruits: ${items.collect { "[$it]" }.join(', ')}"
// Conditional nesting
def user = [name: 'Alice', admin: true]
println "User: ${user.name} ${user.admin ? '(Admin)' : '(Regular)'}"
// Multi-level nesting
def data = [lang: 'Groovy', version: '5.0']
println "Running ${"${data.lang} v${data.version}".toUpperCase()}"
Output
Welcome, John Doe! Fruits: [apple], [banana], [cherry] User: Alice (Admin) Running GROOVY V5.0
What happened here: GStrings can nest arbitrarily deep. The inner expressions evaluate first, and their results are interpolated into the outer GString. This is particularly useful with closures inside collect or findAll where you build formatted strings from collections.
Example 8: Escaping the Dollar Sign
What we’re doing: Showing how to include a literal $ in double-quoted strings.
Example 8: Escaping Dollar Signs
def price = 49.99
// Escape with backslash to get a literal $
println "Price: \$${price}"
println "Total: \$${(price * 1.08).round(2)}"
// Multiple dollar signs
println "Currency: \$USD"
println "Regex: \$\\d+"
// If you don't need interpolation, use single quotes
println 'Price: $49.99' // No escaping needed
// Shell-style variables (escaped)
println "Bash: \$HOME"
def home = "/home/user"
println "Groovy: ${home}"
// Escaping inside triple-double-quoted strings
println """
Invoice:
Item: Widget
Price: \$${price}
Tax: \$${(price * 0.08).round(2)}
Total: \$${(price * 1.08).round(2)}
"""
Output
Price: $49.99 Total: $53.99 Currency: $USD Regex: $\d+ Price: $49.99 Bash: $HOME Groovy: /home/user Invoice: Item: Widget Price: $49.99 Tax: $4.00 Total: $53.99
What happened here: Use \$ to produce a literal dollar sign in double-quoted strings. If your string has many dollar signs and no interpolation needed, just use single quotes instead – much cleaner. This comes up frequently when generating shell scripts, SQL queries with parameters, or displaying currency.
Example 9: GString toString() Internals
What we’re doing: Exploring how GString builds its final string from values and strings arrays.
Example 9: GString Internals
def a = "Hello"
def b = "World"
def gs = "${a}, ${b}!"
// A GString stores strings[] and values[] separately
println "Type: ${gs.getClass().name}"
println "String value: ${gs.toString()}"
// Access the internal string parts
println "Strings: ${gs.strings}"
println "Values: ${gs.values}"
// The GString is composed of interleaved strings and values:
// strings[0] + values[0] + strings[1] + values[1] + strings[2]
// "" + "Hello" + ", " + "World" + "!"
println "strings.length: ${gs.strings.length}"
println "values.length: ${gs.values.length}"
// getValueCount() method
println "Value count: ${gs.getValueCount()}"
// GString with a single interpolation
def simple = "Hi ${a}"
println "Simple strings: ${simple.strings}"
println "Simple values: ${simple.values}"
Output
Type: org.codehaus.groovy.runtime.GStringImpl String value: Hello, World! Strings: [, , , !] Values: [Hello, World] strings.length: 3 values.length: 2 Value count: 2 Simple strings: [Hi , ] Simple values: [Hello]
What happened here: Internally, a GString stores two arrays: strings[] (the literal text fragments) and values[] (the interpolated values). When toString() is called, Groovy interleaves them together. There is always one more string fragment than values – even if the GString starts or ends with an interpolation, the corresponding string fragment is an empty string.
Example 10: GString in SQL – Injection Risk
What we’re doing: Showing the difference between unsafe GString SQL and safe parameterized queries with Groovy SQL.
Example 10: GString and SQL Safety
// UNSAFE: Direct string interpolation in SQL
def userInput = "admin'; DROP TABLE users; --"
// This builds a dangerous query string!
def unsafeQuery = "SELECT * FROM users WHERE name = '${userInput}'"
println "UNSAFE query: ${unsafeQuery}"
// SAFE: Groovy SQL uses GString for parameterized queries
// When you pass a GString to Groovy's Sql.execute(), it
// automatically extracts values as bind parameters
def safeName = "Alice"
def safeQuery = "SELECT * FROM users WHERE name = ${safeName}"
println "GString for Sql: ${safeQuery}"
println "Values array: ${safeQuery.values}"
println "Strings array: ${safeQuery.strings}"
// Groovy Sql would turn this into:
// "SELECT * FROM users WHERE name = ?" with params ["Alice"]
// Building safe queries the right way
def table = 'orders'
def status = 'pending'
def limit = 10
def query = "SELECT * FROM ${table} WHERE status = ${status} LIMIT ${limit}"
println "\nQuery template: ${query.strings.join('?')}"
println "Parameters: ${query.values}"
Output
UNSAFE query: SELECT * FROM users WHERE name = 'admin'; DROP TABLE users; --' GString for Sql: SELECT * FROM users WHERE name = Alice Values array: [Alice] Strings array: [SELECT * FROM users WHERE name = , ] Query template: SELECT * FROM ? WHERE status = ? LIMIT ? Parameters: [orders, pending, 10]
What happened here: This is a critical point. If you concatenate user input directly into SQL strings, you get SQL injection. But Groovy’s groovy.sql.Sql class is clever – when you pass a GString, it inspects the strings[] and values[] arrays and turns interpolated values into bind parameters (question marks). This gives you the convenience of interpolation syntax with the safety of parameterized queries. However, be careful with table names and column names – those cannot be parameterized.
Security Note: Never concatenate user input into SQL strings with
+or single-quoted strings. Always use GString interpolation withgroovy.sql.Sql– it automatically parameterizes the query. For table/column names, validate against a whitelist.
GString vs String – Key Differences
Let’s put all the differences between GString and String in one place. This is a question that comes up constantly in interviews and code reviews.
| Feature | String | GString |
|---|---|---|
| Java class | java.lang.String | groovy.lang.GString |
| Created with | Single quotes '...' | Double quotes with $ |
| Interpolation | No | Yes |
| Immutable | Yes | Values captured at creation (eager) or re-evaluated (lazy) |
| Implements | CharSequence, Comparable, Serializable | CharSequence, Comparable, Serializable, Writable |
| Safe as Map key | Yes | No (hashCode/equals mismatch with String) |
| == comparison | Content-based | Content-based (works with String too) |
| .equals() | Type-strict | Type-strict (GString != String) |
| Coercion | N/A | Auto-coerced to String when needed |
GString vs String Summary
def x = "test"
// Type check
def s = 'hello' // String
def g = "hello ${x}" // GString
def d = "hello" // String! (no $ = no GString)
println "s: ${s.class.simpleName}"
println "g: ${g.class.simpleName}"
println "d: ${d.class.simpleName}"
// Auto-coercion in action
void printString(String s) { println "Received String: $s" }
printString("Value: ${x}") // GString auto-coerced to String
// Where coercion does NOT happen: HashMap keys
def map1 = new HashMap()
map1.put("${x}", "gstring-key")
map1.put(x, "string-key")
println "HashMap entries: ${map1.size()}" // 2, not 1!
Output
s: String g: GStringImpl d: String Received String: Value: test HashMap entries: 2
The auto-coercion works when the target type is known at compile time (method parameters typed as String). But when you pass a GString to a generic API like HashMap.put(Object, Object), no coercion happens – the GString stays as-is.
Common Pitfalls and Gotchas
Pitfall 1: Dotted Access Without Braces Can Break
Pitfall: Dotted Access
def user = [name: 'Alice']
// This works - simple dotted access
println "Name: $user.name"
// This BREAKS - method call needs braces
// println "Upper: $user.name.toUpperCase()" // ERROR!
// Fix: use braces for method calls
println "Upper: ${user.name.toUpperCase()}"
Output
Name: Alice Upper: ALICE
The $variable.property shorthand only works for simple property access chains. The moment you need parentheses (method calls), you must use ${} braces. My recommendation: always use braces. It costs nothing and prevents surprises.
Pitfall 2: GString and Regular Expressions
Pitfall: GString in Regex
// Dollar sign in regex patterns
def text = 'Price is $100'
// Using slashy string ($ is interpolated in slashy strings too!)
// def pattern = /$\d+/ // This would try to interpolate $\d
// Fix 1: Escape the dollar in slashy string
def pattern1 = /\$\d+/
def matcher1 = text =~ pattern1
println "Match: ${matcher1[0]}"
// Fix 2: Use single-quoted string (no interpolation)
def pattern2 = '\\$\\d+'
println "Pattern2 match: ${(text =~ pattern2)[0]}"
Output
Match: $100 Pattern2 match: $100
Pitfall 3: Multiline GString Indentation
Pitfall: Indentation in Multiline GStrings
def name = "World"
// Problem: leading whitespace in multiline GString
def msg1 = """
Hello ${name}
Welcome aboard
"""
println "With whitespace:${msg1}"
// Fix: use stripIndent() (Groovy GDK method)
def msg2 = """\
Hello ${name}
Welcome aboard""".stripIndent()
println "Stripped: ${msg2}"
// Fix: use stripMargin() with a delimiter
def msg3 = """\
|Hello ${name}
|Welcome aboard""".stripMargin()
println "Margin stripped: ${msg3}"
Output
With whitespace:
Hello World
Welcome aboard
Stripped: Hello World
Welcome aboard
Margin stripped: Hello World
Welcome aboard
Multiline GStrings with triple double-quotes preserve all whitespace including indentation. Use stripIndent() or stripMargin() to clean it up. We cover this in detail in the Groovy Multiline String post.
Real-World GString Patterns
Building URLs
Real-World: Building URLs
def baseUrl = "https://api.example.com"
def version = "v2"
def resource = "users"
def userId = 42
def format = "json"
// Clean URL building with GString
def url = "${baseUrl}/${version}/${resource}/${userId}?format=${format}"
println url
// With query parameters from a map
def params = [page: 1, limit: 20, sort: 'name', order: 'asc']
def queryString = params.collect { k, v -> "${k}=${v}" }.join('&')
def fullUrl = "${baseUrl}/${version}/${resource}?${queryString}"
println fullUrl
// URL encoding user input
def searchTerm = "groovy gstring"
def encoded = URLEncoder.encode(searchTerm, 'UTF-8')
def searchUrl = "${baseUrl}/search?q=${encoded}"
println searchUrl
Output
https://api.example.com/v2/users/42?format=json https://api.example.com/v2/users?page=1&limit=20&sort=name&order=asc https://api.example.com/search?q=groovy+gstring
Building SQL Queries
Real-World: Building SQL Queries
def tableName = "employees"
def department = "Engineering"
def minSalary = 50000
// For Groovy Sql - GString becomes parameterized query
def selectQuery = """\
SELECT id, name, salary
FROM ${tableName}
WHERE department = ${department}
AND salary > ${minSalary}
ORDER BY salary DESC"""
println "Query: ${selectQuery.stripIndent()}"
println "Values that would be parameterized: ${selectQuery.values}"
// Dynamic WHERE clause building
def filters = [department: 'Engineering', active: true]
def whereClauses = filters.collect { k, v -> "${k} = ${v}" }
def dynamicQuery = "SELECT * FROM employees WHERE ${whereClauses.join(' AND ')}"
println "\nDynamic: ${dynamicQuery}"
Output
Query: SELECT id, name, salary FROM employees WHERE department = Engineering AND salary > 50000 ORDER BY salary DESC Values that would be parameterized: [employees, Engineering, 50000] Dynamic: SELECT * FROM employees WHERE department = Engineering AND active = true
Template Engine Style Patterns
Real-World: Template Patterns
// Email template using GString
def sendWelcomeEmail(Map user) {
def template = """\
|Dear ${user.name},
|
|Welcome to ${user.company}! Your account has been created.
|
|Username: ${user.email}
|Role: ${user.role ?: 'Member'}
|Created: ${new Date().format('yyyy-MM-dd')}
|
|Best regards,
|The ${user.company} Team""".stripMargin()
return template
}
def email = sendWelcomeEmail(
name: 'Alice Johnson',
company: 'TechCorp',
email: 'alice@techcorp.com',
role: 'Admin'
)
println email
// HTML generation
def items = ['Groovy', 'Grails', 'Gradle']
def html = """\
|<ul>
|${items.collect { " <li>${it}</li>" }.join('\n')}
|</ul>""".stripMargin()
println "\n${html}"
Output
Dear Alice Johnson, Welcome to TechCorp! Your account has been created. Username: alice@techcorp.com Role: Admin Created: 2026-03-08 Best regards, The TechCorp Team <ul> <li>Groovy</li> <li>Grails</li> <li>Gradle</li> </ul>
For simple templates, GStrings with stripMargin() work beautifully. For more complex templating with loops and conditionals, consider Groovy’s built-in template engines like SimpleTemplateEngine or StreamingTemplateEngine.
Performance Considerations
GStrings have a small overhead compared to plain String concatenation because they maintain the strings[] and values[] arrays. However, in practice, this overhead is negligible for almost all use cases.
Performance: GString vs Concatenation
def name = "World"
def iterations = 100_000
// Approach 1: GString
def start1 = System.nanoTime()
iterations.times { "Hello, ${name}! Welcome.".toString() }
def time1 = (System.nanoTime() - start1) / 1_000_000
// Approach 2: String concatenation
def start2 = System.nanoTime()
iterations.times { "Hello, " + name + "! Welcome." }
def time2 = (System.nanoTime() - start2) / 1_000_000
// Approach 3: StringBuilder
def start3 = System.nanoTime()
iterations.times {
new StringBuilder("Hello, ").append(name).append("! Welcome.").toString()
}
def time3 = (System.nanoTime() - start3) / 1_000_000
println "GString: ${time1}ms"
println "Concatenation: ${time2}ms"
println "StringBuilder: ${time3}ms"
Output (approximate)
GString: 45ms Concatenation: 38ms StringBuilder: 42ms
For simple interpolations, all three approaches perform similarly. GString has the best readability, and the performance difference is in the microsecond range per operation. Only reach for StringBuilder when you are building strings in a tight loop with hundreds of append operations.
Performance Tip: If you pass a GString to a method that calls
toString()multiple times (like logging), the lazy closure form${-> expr}will re-evaluate each time. For expensive expressions, either cache the result or use eager interpolation${expr}.
Conclusion
Groovy GString is one of those features that makes you genuinely productive. Instead of fighting with String.format() or chaining StringBuilder calls, you write natural, readable interpolated strings with ${}. The lazy evaluation with closures, the auto-parameterization in Groovy SQL, and the smooth nesting make GStrings genuinely useful.
But remember the gotchas: GStrings and Strings are different types, so never use GStrings as map keys without calling .toString(). Use == for comparison (not .equals()). And always escape \$ when you need a literal dollar sign in double-quoted strings.
If you haven’t already, check out our complete Groovy String tutorial for the broader picture, and look forward to our next post on Groovy multiline strings where we combine triple-quoted strings with GString interpolation for flexible text templating.
Summary
- GStrings use
${expression}for interpolation inside double-quoted strings - Use
${-> closure}for lazy evaluation that re-evaluates each time - GString is NOT a java.lang.String – it is
groovy.lang.GString - Never use GStrings as Map keys – call
.toString()first - Groovy SQL auto-parameterizes GString values, preventing SQL injection
- Escape dollar signs with
\$in double-quoted strings - Use
==to compare GStrings with Strings – it compares by content
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Multiline String – Heredoc and Triple Quotes
Frequently Asked Questions
What is a GString in Groovy?
A GString is Groovy’s interpolated string type (groovy.lang.GString). It is created when you use double quotes with a ${expression} inside. The expression is evaluated and its toString() result is embedded in the string. Without any $ expression, a double-quoted string is a plain java.lang.String.
What is the difference between GString and String in Groovy?
A String (java.lang.String) is created with single quotes and has no interpolation. A GString (groovy.lang.GString) is created with double quotes containing ${} expressions. They are different types – instanceof String returns false for GString. Groovy auto-coerces GStrings to Strings in most cases, but they behave differently as Map keys and with .equals().
How does lazy evaluation work in Groovy GString?
Use the closure form ${-> expression} inside a GString. Instead of capturing the value at creation time, a closure is stored and re-evaluated every time toString() is called. This is useful for logging, debugging, and any scenario where you want the most current value.
Why should I avoid using GString as a Map key?
Because GString.equals(String) returns false even when the content is identical. Java’s HashMap uses both hashCode() and equals() for key lookup. A GString key and a String key with the same text are treated as different keys, leading to duplicate entries. Always call .toString() on GString keys or use single-quoted strings.
Is Groovy GString safe for SQL queries?
When used with Groovy’s groovy.sql.Sql class, yes. Groovy SQL inspects the GString‘s internal strings[] and values[] arrays and automatically converts interpolated values into parameterized bind variables (question marks), preventing SQL injection. However, never use GString concatenation with raw JDBC – always use Groovy’s Sql class for this safety feature.
Related Posts
Previous in Series: Groovy Split String – All Methods Explained
Next in Series: Groovy Multiline String – Heredoc and Triple Quotes
Related Topics You Might Like:
- Groovy String Tutorial – The Complete Guide
- Groovy Multiline String – Heredoc and Triple Quotes
- Groovy Compare Strings – equals, compareTo, and More
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment