diff --git a/scripts/qapi/golang/__init__.py b/scripts/qapi/golang/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
new file mode 100644
index 0000000000..333741b47b
--- /dev/null
+++ b/scripts/qapi/golang/golang.py
@@ -0,0 +1,135 @@
+"""
+Golang QAPI generator
+"""
+
+# Copyright (c) 2025 Red Hat Inc.
+#
+# Authors:
+#  Victor Toso <victortoso@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+# Just for type hint on self
+from __future__ import annotations
+
+import os, shutil
+from typing import List, Optional
+
+from ..schema import (
+    QAPISchema,
+    QAPISchemaBranches,
+    QAPISchemaEnumMember,
+    QAPISchemaFeature,
+    QAPISchemaIfCond,
+    QAPISchemaObjectType,
+    QAPISchemaObjectTypeMember,
+    QAPISchemaType,
+    QAPISchemaVariants,
+    QAPISchemaVisitor,
+)
+from ..source import QAPISourceInfo
+
+
+def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
+    vis = QAPISchemaGenGolangVisitor(prefix)
+    schema.visit(vis)
+    vis.write(output_dir)
+
+
+class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
+    # pylint: disable=too-many-arguments
+    def __init__(self, _: str):
+        super().__init__()
+        gofiles = ("protocol.go",)
+        self.schema: QAPISchema
+        self.golang_package_name = "qapi"
+        self.duplicate = list(gofiles)
+
+    def visit_begin(self, schema: QAPISchema) -> None:
+        self.schema = schema
+
+    def visit_end(self) -> None:
+        del self.schema
+
+    def visit_object_type(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        base: Optional[QAPISchemaObjectType],
+        members: List[QAPISchemaObjectTypeMember],
+        branches: Optional[QAPISchemaBranches],
+    ) -> None:
+        pass
+
+    def visit_alternate_type(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        variants: QAPISchemaVariants,
+    ) -> None:
+        pass
+
+    def visit_enum_type(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        members: List[QAPISchemaEnumMember],
+        prefix: Optional[str],
+    ) -> None:
+        pass
+
+    def visit_array_type(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        element_type: QAPISchemaType,
+    ) -> None:
+        pass
+
+    def visit_command(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        arg_type: Optional[QAPISchemaObjectType],
+        ret_type: Optional[QAPISchemaType],
+        gen: bool,
+        success_response: bool,
+        boxed: bool,
+        allow_oob: bool,
+        allow_preconfig: bool,
+        coroutine: bool,
+    ) -> None:
+        pass
+
+    def visit_event(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        arg_type: Optional[QAPISchemaObjectType],
+        boxed: bool,
+    ) -> None:
+        pass
+
+    def write(self, outdir: str) -> None:
+        godir = "go"
+        targetpath = os.path.join(outdir, godir)
+        os.makedirs(targetpath, exist_ok=True)
+
+        # Content to be copied over
+        srcdir = os.path.dirname(os.path.realpath(__file__))
+        for filename in self.duplicate:
+            srcpath = os.path.join(srcdir, filename)
+            dstpath = os.path.join(targetpath, filename)
+            shutil.copyfile(srcpath, dstpath)
diff --git a/scripts/qapi/golang/protocol.go b/scripts/qapi/golang/protocol.go
new file mode 100644
index 0000000000..4ff8d2f3fb
--- /dev/null
+++ b/scripts/qapi/golang/protocol.go
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2025 Red Hat, Inc.
+ * SPDX-License-Identifier: MIT-0
+ *
+ * Authors:
+ *  Victor Toso <victortoso@redhat.com>
+ *  Daniel P. Berrange <berrange@redhat.com>
+ */
+package qapi
+
+import (
+	"encoding/json"
+	"time"
+)
+
+/* Union of data for command, response, error, or event,
+ * since when receiving we don't know upfront which we
+ * must deserialize */
+type Message struct {
+	QMP       *json.RawMessage `json:"QMP,omitempty"`
+	Execute   string           `json:"execute,omitempty"`
+	ExecOOB   string           `json:"exec-oob,omitempty"`
+	Event     string           `json:"event,omitempty"`
+	Error     *json.RawMessage `json:"error,omitempty"`
+	Return    *json.RawMessage `json:"return,omitempty"`
+	ID        string           `json:"id,omitempty"`
+	Timestamp *Timestamp       `json:"timestamp,omitempty"`
+	Data      *json.RawMessage `json:"data,omitempty"`
+	Arguments *json.RawMessage `json:"arguments,omitempty"`
+}
+
+type QAPIError struct {
+	Class       string `json:"class"`
+	Description string `json:"desc"`
+}
+
+func (err *QAPIError) Error() string {
+	return err.Description
+}
+
+type Timestamp struct {
+	Seconds      int `json:"seconds"`
+	MicroSeconds int `json:"microseconds"`
+}
+
+func (t *Timestamp) AsTime() time.Time {
+	return time.Unix(int64(t.Seconds), int64(t.MicroSeconds)*1000)
+}
diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
index 324081b9fc..af315c1ad1 100644
--- a/scripts/qapi/main.py
+++ b/scripts/qapi/main.py
@@ -16,6 +16,7 @@
 from .error import QAPIError
 from .events import gen_events
 from .features import gen_features
+from .golang import golang
 from .introspect import gen_introspect
 from .schema import QAPISchema
 from .types import gen_types
@@ -55,6 +56,7 @@ def generate(schema_file: str,
     gen_commands(schema, output_dir, prefix, gen_tracing)
     gen_events(schema, output_dir, prefix)
     gen_introspect(schema, output_dir, prefix, unmask)
+    golang.gen_golang(schema, output_dir, prefix)
 
 
 def main() -> int:
