Accessing MVS Facilities in Go Development on z/OS
The mainframe isn't going anywhere—and neither is the need to modernize how we interact with it. As organizations embrace cloud-native languages like Go for their simplicity, performance, and excellent concurrency model many z/OS workloads still require leveraging MVS. Go on z/OS provides a powerful bridge between these worlds, enabling developers to write high-performance, scalable applications that interact seamlessly with MVS resources.
There are multiple different approaches that are available. Each with their own pros and cons. In this blog we will explore each approach, when to use them, and provide some examples to help you get started.
The Three Approaches
CGO — Call C code that accesses MVS macros (requires IBM Open XL C/C++)
Go Assembly — Write s390x assembly directly in Go
Dynamic Module Loading — Load existing assembly modules at runtime
Option 1: CGO with IBM Open XL C/C++
CGO provides a bridge between Go and C code, making it an excellent choice when you need to integrate existing C libraries or access MVS macros through C wrappers. However, it does come with the performance overheads of CGO and some additional requirements that you will be locked into.
Use when:
- You need to call existing C libraries
- You are more comfortable with C than Go Assembly
- You need MVS macro support
Important: Only IBM Open XL C/C++ for z/OS is supported with CGO. xlclang and xlc/metal C are not supported.
You can embed assembly code directly in C to access MVS macros. For instance, the zoslib project demonstrates how to embed assembly code for operations like WTO (Write To Operator). You can find practical examples in the zoslib repository showing how to wrap MVS functionality in C code (which are too big to display here). An important constraint with this approach is to make sure you never allow signals generated from your C code to propagate back to Go. This can cause runtime instability and unexpected behavior.
Note that when using CGO the following configuration is required:
- xplink64 linkage
- norent
Note that when working with CGO static linking is the default (and the implementation is entirely in the C embed). However, you can use a .x file to enable dynamic loading on call.
Pros:
- Supported by CGO tool
- Access to low level MVS macros through familiar C syntax
- Static linking is the default behavior, but dynamic linking is possible with .x files
Cons:
- Requires IBM Open XL compiler
- Creates additional build complexity
- Runtime overhead introduced by CGO for every cross-language call
Option 2: Pure Go Assembly (Recommended)
For performance-critical code or when you want to avoid CGO overhead entirely, writing pure Go assembly is a better option. This approach is the recommended option as it doesn't involve the performance hits and extra build/runtime complexity of the other methods.
Use when:
- You need direct, tightly coupled implementation
- Maximum performance is required (or you just want to avoid the CGO overhead)
- You're implementing additional low-level functionality
Go assembly allows you to write s390x assembly code that integrates directly with your Go programs. The go-recordio project provides examples of this approach, including implementations of utility functions that interact directly with system facilities. To use this approach you can set up a Go function in assembly like so:
TEXT ·Deref(SB), NOSPLIT, $0-24
MOVD ptr+0(FP), R10 // test pointer in R10
MOVD $0x0, R6
// PLATFORM SPECIFIC ASSEMBLY GOES HERE
MOVD R6, value+8(FP) // result in R6
MOVD R3, error+16(FP) // error in R3
RET
Important: Go assembly code must follow the Go ABI0 specification
Go ASM has its own set of supported instructions, meaning many of the necessary opcodes don't have a Go ASM equivalent. To get around this we can write the instructions as binary data like so:
TEXT ·Deref(SB), NOSPLIT, $0-24
MOVD ptr+0(FP), R10 // test pointer in R10
MOVD $0x0, R6
// START OF PLATFORM SPECIFIC CODE
BYTE $0xE3; BYTE $0x20; BYTE $0x04; BYTE $0xB8; BYTE $0x00; BYTE $0x17 // llgt 2,1208
BYTE $0xB9; BYTE $0x17; BYTE $0x00; BYTE $0x22 // llgtr 2,2
BYTE $0xA5; BYTE $0x26; BYTE $0x7F; BYTE $0xFF // nilh 2,32767
BYTE $0xE3; BYTE $0x22; BYTE $0x00; BYTE $0x58; BYTE $0x00; BYTE $0x04 // lg 2,88(2)
BYTE $0xE3; BYTE $0x22; BYTE $0x00; BYTE $0x08; BYTE $0x00; BYTE $0x04 // lg 2,8(2)
BYTE $0x41; BYTE $0x22; BYTE $0x03; BYTE $0x68 // la 2,872(2)
BYTE $0xB9; BYTE $0x82; BYTE $0x00; BYTE $0x33 // xgr 3,3
BYTE $0xA7; BYTE $0x55; BYTE $0x00; BYTE $0x04 // bras 5,lbl1
BYTE $0xA7; BYTE $0x39; BYTE $0x00; BYTE $0x01 // lghi 3,1
BYTE $0xB9; BYTE $0x02; BYTE $0x00; BYTE $0x33 // lbl1 ltgr 3,3
BYTE $0xA7; BYTE $0x74; BYTE $0x00; BYTE $0x08 // brc b'0111',lbl2
BYTE $0xE3; BYTE $0x52; BYTE $0x00; BYTE $0x00; BYTE $0x00; BYTE $0x24 // stg 5,0(2)
BYTE $0xE3; BYTE $0x6A; BYTE $0x00; BYTE $0x00; BYTE $0x00; BYTE $0x04 // lg 6,0(10)
BYTE $0xB9; BYTE $0x82; BYTE $0x00; BYTE $0x99 // lbl2 xgr 9,9
BYTE $0xE3; BYTE $0x92; BYTE $0x00; BYTE $0x00; BYTE $0x00; BYTE $0x24 // stg 9,0(2)
// END OF PLATFORM SPECIFIC CODE
MOVD R6, value+8(FP) // result in R6
MOVD R3, error+16(FP) // error in R3
RET
Pros:
- Zero CGO overhead
- Direct control over implementation details
- Best performance characteristics
- No external compiler dependencies
Cons:
- Requires Go and s390x assembly knowledge
- Cannot be easily integrated with legacy non-Go code
Option 3: Dynamic Module Loading
This option should only be used for scenarios involving legacy assembly code or when you need runtime flexibility. However if possible it is recommended to re-write these modules in Go assembly to remove the additional runtime management and additional overhead.
Use when:
- You have existing assembly modules
- You need to load code at runtime
- You're working with AMODE 24/31/64 code
This method involves storing assembly code in PDSE members and loading modules at runtime using SVC 8. However, when loading dynamic modules, always account for AMODE requirements and constraints. The go-recordio project includes examples of this approach, including WTO (Write To Operator) implementations.
Important: When loading dynamic modules, always account for AMODE requirements and constraints.
Pros:
- Can reuse existing assembly modules
- Flexible runtime loading
- Supports multiple AMODE
Cons:
- Not directly linked to Go
- More complex runtime management
- Requires PDSE setup
Final thoughts
Keep in mind that Go assumes ASCII encoded data, be aware of EBCDIC-ness in assembly code and convert character data when needed to ensure proper operation. Finally, when choosing an approach always choose the approach that works best for you and your constraints, such as what you or your team are comfortable with, what performance you require, and what legacy code you may already have.