用 Go 开发鸿蒙 PC(2in1)二维码生成应用:从环境搭建到落地的完整实战
欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/
目标平台:HarmonyOS PC / 2in1。需求:在鸿蒙 PC 上做一个二维码生成器——
用户输入文本/网址 → 生成二维码 → 预览 → 保存到图库。
主线:用 Go(官方移植版ohos_golang_go)做二维码核心,ArkTS 做桌面 UI,
中间用 NAPI 搭桥。本文把环境搭建、工程实现、集成踩坑、最终落地与验证一次性讲透,
看完即可复现。

最终效果:应用以桌面窗口形态运行在鸿蒙 PC(2in1)上,输入内容即生成二维码。
目录
- 为什么是鸿蒙 PC + Go
- 环境搭建
- 工程搭建
- 用 Go 写二维码核心并交叉编译
- 集成的两个关键现实(避坑核心)
- 务实落地:C++ 写核心 + ArkTS UI
- 构建 / 安装 / 运行
- 验证:不止能跑,还要正确
- 结论与展望
1. 为什么是鸿蒙 PC + Go
鸿蒙 PC(2in1 形态,如 MateBook 系列)与手机共用同一套 OHOS 应用模型:UI 用 ArkTS,
原生能力走 NAPI。把应用声明为 2in1 设备类型即可在 PC 上运行。
选 Go 做核心,是因为 Go 的 skip2/go-qrcode 成熟、生成 PNG 只要几行。剩下的真正难题是:
怎么把 Go 编出来的东西塞进鸿蒙 PC 应用。 这是全文重点。
2. 环境搭建
2.1 系统与工具清单
本文环境为 macOS(Apple Silicon, arm64)+ zsh。实测可用的工具链:
| 工具 | 版本 | 用途 |
|---|---|---|
| Go(系统版) | go1.24.3 darwin/arm64 | 仅作 ohos_golang_go 的 bootstrap |
| ohos_golang_go | 基于 go1.22.10 | 交叉编译 GOOS=openharmony 的 Go 工具链 |
ark-cli (ark) |
0.1.2 | 鸿蒙命令行脚手架(建项目/构建/安装/模拟器) |
hvigor (hvigorw) |
6.24.2 | 鸿蒙工程构建系统 |
| ohpm | 6.1.2 | 鸿蒙包管理器 |
| node | v23.11.0 | hvigor 运行时 |
| OpenHarmony SDK | API 24 / 6.1.1.125 | ArkTS 编译、NDK |
| NDK clang | OHOS clang 15.0.4 | C/C++ 交叉编译(aarch64-unknown-linux-ohos) |
| hdc | 随 SDK | 设备/模拟器调试 |
2.2 SDK 与 NDK 路径(重要坑)
环境变量 OHOS_NDK_HOME / OHOS_SDK 指向的 ~/Library/OpenHarmony/Sdk 实际不存在(失效配置)。
真实 SDK 位于:
~/command-line-tools/sdk/default/openharmony/native # NDK(clang、sysroot)
~/command-line-tools/sdk/default/openharmony/toolchains/hdc # hdc
NDK 交叉编译器(按目标架构提供 wrapper):
.../native/llvm/bin/aarch64-unknown-linux-ohos-clang # arm64
.../native/llvm/bin/x86_64-unknown-linux-ohos-clang # x86_64
# 验证
aarch64-unknown-linux-ohos-clang --version
# OHOS (dev) clang version 15.0.4 Target: aarch64-unknown-linux-ohos
脚本里应优先用环境变量,失效时回退到
~/command-line-tools/sdk/default/openharmony/native。
2.3 安装 ark-cli 与统一运行时
ark-cli(二进制名 ark)是轻量级命令行脚手架,内嵌 hdc,覆盖建项目/构建/安装/调试/模拟器,
可在无完整 DevEco Studio 时完成开发闭环。
第一步:安装本体(三选一)
# 方式一:克隆仓库安装(推荐,脚本自动编译并装入 PATH、建 ark 软链)
git clone https://atomgit.com/nutpi/ark-cli.git && cd ark-cli && bash scripts/install.sh
# 方式二:从源码构建(需 Rust:curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh)
git clone https://atomgit.com/nutpi/ark-cli.git && cd ark-cli
cargo build --release
cp target/release/ark-cli /usr/local/bin/ && ln -sf /usr/local/bin/ark-cli /usr/local/bin/ark
# 方式三:从 Releases 下载对应平台预编译二进制,放入 PATH
第二步:安装统一运行时(关键,否则 build/run/emulator 不可用)
ark runtime install # 下载 SDK + hvigor/ohpm/node + Emulator 到 ~/.ark-cli/runtime/
ark runtime status # 各组件状态,自愈失效路径
ark --version # 0.1.2
ark devices # 验证 hdc 与设备连接
2.4 构建 ohos_golang_go 移植版(关键)
标准 Go 不认识 GOOS=openharmony,必须用 OpenHarmony-SIG 的移植版:
仓库:https://gitee.com/openharmony-sig/ohos_golang_go(基于 go1.22.10)。
它新增了 GOOS=openharmony(runtime 含 rt0_openharmony_arm64.s 等),当前仅支持 arm64。
# 1) 克隆(仓库 ~180MB,浅克隆即可)
cd /Volumes/coder/hmapp/qrcode
git clone --depth 1 https://gitee.com/openharmony-sig/ohos_golang_go.git ohos-go
# 2) 用系统 Go 作 bootstrap,编译工具链(约 1-3 分钟)
cd ohos-go/src
GOROOT_BOOTSTRAP="$(go env GOROOT)" ./make.bash
# 3) 验证:产出 ohos-go/bin/go
../bin/go version # go version go1.22.10 darwin/arm64
2.5 模拟器准备(鸿蒙 PC / 2in1)
ark emulator list # 查看实例与已下载镜像
ark emulator start MateBook Pro # 启动 2in1(PC)模拟器,API 24
.../toolchains/hdc list targets # 127.0.0.1:5555 Connected
本机用
ark内嵌 hdc 偶报“文件不存在”,改用 SDK 自带~/command-line-tools/sdk/default/openharmony/toolchains/hdc直连127.0.0.1:5555稳定可用。
2.6 环境自检
go version # 系统 Go(bootstrap)
/Volumes/coder/hmapp/qrcode/ohos-go/bin/go version # 移植版 Go(openharmony)
ark --version # ark-cli
aarch64-unknown-linux-ohos-clang --version # NDK clang
.../toolchains/hdc list targets # 模拟器在线
3. 工程搭建
3.1 创建工程
ark create qrapp --template default --bundle com.example.qrapp
default 模板不含 native,故手动添加 cpp/ 目录与构建配置。
3.2 工程结构
qrapp/
├─ entry/
│ ├─ src/main/
│ │ ├─ ets/
│ │ │ ├─ pages/Index.ets # UI:输入/生成/预览/保存
│ │ │ └─ utils/QrService.ets # 业务:原生调用/PixelMap/PNG/存图库
│ │ ├─ cpp/
│ │ │ ├─ napi_init.cpp # NAPI 包装(C++ 生成二维码 RGBA)
│ │ │ ├─ CMakeLists.txt # 编译 libentry.so + qrcodegen
│ │ │ ├─ qrcodegen/ # Nayuki qrcodegen (MIT)
│ │ │ └─ types/libentry/ # .so 的 ArkTS 类型声明
│ │ └─ module.json5 # deviceTypes 含 2in1
│ ├─ build-profile.json5 # externalNativeOptions / abiFilters
│ └─ oh-package.json5 # 依赖 libentry.so
├─ gocore/ # 【归档】Go 二维码核心 + 交叉编译脚本
│ ├─ cmd/qrexec/main.go # Go 可执行程序(已验证可在 OHOS 运行)
│ ├─ build-ohos.sh # 用 ohos_golang_go 交叉编译
│ └─ go.mod / go.sum
└─ docs/
声明目标设备类型为鸿蒙 PC(2in1):
// entry/src/main/module.json5
"deviceTypes": ["2in1", "tablet", "phone"]
4. 用 Go 写二维码核心并交叉编译
核心逻辑(gocore/):
import qrcode "github.com/skip2/go-qrcode"
// 生成中等纠错的 PNG 字节流
png, err := qrcode.Encode(content, qrcode.Medium, size)
用移植版交叉编译为 OHOS 产物(注意两个关键开关):
export GOROOT=/Volumes/coder/hmapp/qrcode/ohos-go
export GOTOOLCHAIN=local # 坑1:否则 go.mod 的 go 指令会触发下载标准版 Go,前功尽弃
export GOPROXY=https://goproxy.cn,direct # 坑2:默认 proxy 当时一直 Bad Gateway
CGO_ENABLED=1 GOOS=openharmony GOARCH=arm64 \
CC=.../native/llvm/bin/aarch64-unknown-linux-ohos-clang \
$GOROOT/bin/go build -trimpath -o libqrcore.so .
坑位提醒:
go.mod里go 1.24会让工具链尝试下载标准版go1.24.0(不含 openharmony),
需把go.mod降到go 1.22并设GOTOOLCHAIN=local。- 依赖下载用
GOPROXY=https://goproxy.cn,direct。
到这里 Go 一切顺利:产物是合法的 AArch64 ELF、导出符号正常。
真正的考验在「怎么让鸿蒙 PC 应用用上它」。
5. 集成的两个关键现实(避坑核心)
把 Go 接进 OHOS 应用,直觉上有两条路。我都走通到最后一步,再分别被平台拦下——
无论手机还是 PC,结论一致(同一套 OHOS / musl)。
5.1 路线 A:Go 编成 .so,经 NAPI dlopen 加载 —— 被 musl TLS 拒绝
-buildmode=c-shared 编出 libqrcore.so,C++ 写 NAPI 包装层链接它,HAP 打包、装到
鸿蒙 PC、启动——全部成功。HAP 内三个库齐全:
libs/arm64-v8a/libentry.so # NAPI 包装
libs/arm64-v8a/libqrcore.so # Go 核心
libs/arm64-v8a/libc++_shared.so
但点「生成」就报错(hilog):
W C03f04/MMG: Error relocating .../libqrcore.so:
initial-exec TLS resolves to dynamic definition in .../libqrcore.so
load module default/entry failed.. load failed
E C03f00/ArkCompiler: export objects of native so is undefined, so name is ...libentry.so
根因:Go runtime 需要随时拿到当前 goroutine 指针 g,它通过 TLS 访问,用的是
initial-exec(IE) 模型——该模型假设符号所在模块在程序启动时就已加载。
查 .so 重定位果然有一条 IE 的 R_AARCH64_TLS_TPREL64。
而鸿蒙的 libc 是 musl。glibc 为「后期 dlopen 进来又用 IE-TLS」的库预留了 static TLS
surplus 兜底,musl 没有这个机制——重定位时直接判定该 TLS 符号属于 dlopen 模块
(tls_id > static_tls_cnt),IE 无法满足,报错拒绝。
决定性实验:写最小 C 程序在全新进程里直接
dlopen这个.so,照样失败——
排除「surplus 被占满」,坐实是 musl 的根本限制:void* h = dlopen("/data/local/tmp/libqrcore.so", RTLD_NOW); // DLOPEN_FAIL: ... initial-exec TLS resolves to dynamic definition
真正的修复是 Go 上游 PR #75048(给 arm64/amd64
引入 TLSDESC / global-dynamic,musl 对这两种支持 dlopen),但它尚未合并,移植版也没跟进。
这坑不是鸿蒙独有:Alpine(也是 musl)上 Go 的 c-shared + dlopen 同样炸,见
golang/go#54805;sing-box 在鸿蒙上踩过同款
(issue #3681)。
5.2 路线 B:Go 编成可执行程序,fork/exec 子进程 —— 被 W^X 拒绝
IE-TLS 在「进程启动时加载」是合法的(那时模块就在静态 TLS 集合里)。于是把 Go 编成
可执行程序,由 C++ NAPI fork/execve 当子进程调用。
先验证 Go 程序能在鸿蒙上跑——把 Go 改成命令行程序,hdc shell 直接运行:
$ ./qrexec 'https://harmonyos.dev/go' out.png 384
605
$ head -c 8 out.png | xxd
00000000: 8950 4e47 0d0a 1a0a .PNG....
605 字节、PNG 魔数正确——证明 Go 核心在 OHOS 上完全正确,唯一问题是「应用内加载」。
把可执行文件命名为 libqrexec.so 混进 libs/(Android 经典技巧),C++ 用 dladdr 定位、execv 调用。启动、点生成——失败:
E QRGO: exec not X_OK errno=13 path=.../libs/arm64/libqrexec.so # bundle 文件无执行权限
I QRGO: qrexec exit code=113 # 113 = 100 + errno(13=EACCES)
复制到沙箱 cache、chmod 0755 再 execve——仍 EACCES。根因:HarmonyOS 执行
W^X(Write XOR eXecute) 安全模型,应用进程无权 execve 沙箱内任何二进制(只能 dlopen .so)。
这是平台设计,鸿蒙 PC 同样如此。
5.3 小结
| 集成方式 | 结果 | 原因 |
|---|---|---|
NAPI dlopen Go .so |
❌ | musl 不支持 dlopen 含 IE-TLS 的库 |
execve Go 可执行程序 |
❌ | OHOS W^X 禁止应用执行沙箱内二进制 |
当前(HarmonyOS 6.1.1 / API 24)Go 无法嵌入应用运行时。 但 Go 交叉编译这条链路是通的、
Go 程序在 OHOS 上也能跑——这部分成果完整保留,等 PR #75048 落地即可复活纯 Go 方案。
6. 务实落地:C++ 写核心 + ArkTS UI
撞墙后的正确姿势是务实交付:运行时改用 C++(NAPI)实现二维码,Go 交叉编译成果归档。
6.1 原生层(C++ NAPI + qrcodegen)
核心库用 Nayuki qrcodegen
(单文件 C,MIT,久经考验)。C++ 生成矩阵、渲染 RGBA 返回 ArkTS:
// generateQrCode(content, size) -> { data: ArrayBuffer(RGBA), width }
bool ok = qrcodegen_encodeText(content.c_str(), tmp.data(), qr.data(),
qrcodegen_Ecc_MEDIUM, qrcodegen_VERSION_MIN,
qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
int n = qrcodegen_getSize(qr.data()); // 模块边数
// 加 4 模块静默区,按 size 整数倍放大,白底黑码填进 RGBA8888 缓冲...
模块注册(名 entry,对应导入名 libentry.so):
static napi_module qrModule = { .nm_register_func = Init, .nm_modname = "entry", ... };
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&qrModule);
}
CMakeLists.txt:
add_library(entry SHARED napi_init.cpp qrcodegen/qrcodegen.c)
target_link_libraries(entry PUBLIC libace_napi.z.so)
entry/build-profile.json5 启用原生构建:
"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"abiFilters": ["arm64-v8a", "x86_64"]
}
}
entry/oh-package.json5 声明 .so 依赖:
"dependencies": { "libentry.so": "file:./src/main/cpp/types/libentry" }
6.2 业务层(ArkTS)
QrService.ets:
createQr():调原生 →image.createPixelMap(rgba, {pixelFormat: RGBA_8888, size});packToPng():image.createImagePacker().packing(pixelMap, {format:'image/png'});saveToAlbum():photoAccessHelper.createAsset(IMAGE,'png')+fileIo写入。
const qr = generateQrCode(text, 512); // 原生
this.pixelMap = await image.createPixelMap(qr.data,
{ pixelFormat: image.PixelMapFormat.RGBA_8888,
size: { width: qr.width, height: qr.width } });
// SaveButton 回调里
const png = await image.createImagePacker().packing(this.pixelMap,
{ format: 'image/png', quality: 100 });
const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png'); // + fileIo 写入
6.3 UI 层(ArkTS)
Index.ets:TextInput 输入 → Button 生成 → Image(pixelMap) 预览 →
安全控件 SaveButton 保存(点击即获临时授权,无需申请持久化相册权限——桌面端体验尤其顺手)。
SaveButton({ text: SaveDescription.SAVE_IMAGE })
.onClick((event, result) => {
if (result === SaveButtonOnClickResult.SUCCESS) { this.onSave(); }
});
界面状态(下图为模拟器,UI 在手机/PC 形态一致):生成前的空预览区 → 生成后的二维码。
| 初始界面 | 生成二维码 |
|---|---|
![]() |
![]() |
7. 构建 / 安装 / 运行
# (可选,归档) 交叉编译 Go,验证 ohos_golang_go 链路
bash gocore/build-ohos.sh
# 构建 HAP
cd qrapp && ark build --mode debug
# 产物: entry/build/default/outputs/default/entry-default-unsigned.hap
# 安装到鸿蒙 PC 模拟器并启动
HDC=~/command-line-tools/sdk/default/openharmony/toolchains/hdc
$HDC install -r entry/build/default/outputs/default/entry-default-unsigned.hap
$HDC shell aa start -b com.example.qrapp -a EntryAbility
本模拟器接受 unsigned HAP 直接
hdc install;真机/部分模拟器需配置签名
(build-profile.json5的signingConfigs,或用 DevEco 自动签名)。
8. 验证:不止能跑,还要正确
NAPI 模块加载 —— 无 export objects undefined 错误(C++ 方案不含 Go TLS 依赖)。
生成正确性 —— 把界面截图喂 OpenCV 解码:
import cv2
data, _, _ = cv2.QRCodeDetector().detectAndDecode(cv2.imread('screen.png'))
print(data) # https://harmonyos.dev/go-qrcode ← 与输入完全一致
保存到图库 —— 点保存,安全控件弹出系统授权框(仅当次点击有效,无需持久化权限):

授权后,hilog 显示媒体库新建资源、缩略图生成:
MediaLibrary: [update] ... fileId_: 1, ownerAlbumId_: 12 ... thumbnailVisible_: 0 -> 1
MediaLibrary: Thumbnail PostProcess GenRes 1
整个 UI 测试是脚本驱动的:
hdc shell uinput -K -t <文本>输入、uinput -T -c x y点击、snapshot_display截图,再用 OpenCV 校验——全程没点一下手。
| 项 | 结果 |
|---|---|
| HAP 打包 | ✅ libentry.so(含 qrcodegen) + libc++_shared.so,arm64-v8a / x86_64 |
| NAPI 模块加载 | ✅ 无 export objects undefined |
| UI 渲染 | ✅ 标题/输入框/按钮/预览区/保存按钮正常 |
| 生成二维码 | ✅ 预览区显示 |
| 二维码正确性 | ✅ OpenCV 解码 = 输入文本 |
| 保存到图库 | ✅ MediaLibrary 新建资源 + 缩略图 |
9. 结论与展望
- 先验证最危险的假设。 本项目最大风险从来不是写代码,而是「Go 能不能在 OHOS 应用里跑」。
早点做最小 dlopen 实验、hdc 直跑实验,能极大缩短试错链路。 - 报错要追到根。
export objects undefined只是表象,往下是initial-exec TLS resolves to dynamic definition,再往下是 musl 与 glibc 的 TLS 机制差异。 - 平台安全模型是硬约束。 W^X 不是 bug、不是配置,是设计;与它较劲不如绕开。
- 鸿蒙 PC 应用的原生逻辑,目前请用 C/C++(NAPI),UI 用 ArkTS;Go 暂时进不了应用运行时
(dlopen 被 musl IE-TLS 限制、execve 被 W^X 限制,PC 与手机一致)。 - 但 Go 交叉编译链路是通的:
ohos_golang_go能把 Go 编成 OHOS 程序并实跑,适合放在
与设备解耦的服务端/工具链里;待 Go PR #75048
(TLSDESC)合并并被移植版跟进,纯 Go + NAPI 的鸿蒙 PC 应用即可成真。 - 务实交付:本项目最终是一个完整可运行的鸿蒙 PC(2in1)二维码应用(C++ + ArkTS),
并完整保留、文档化了 Go 的交叉编译成果与平台限制分析。
更多推荐






所有评论(0)