基于Go编写一个Windows剪贴板监控器

作者:

文章目录
  • 学习目的:理解 Windows 消息机制、剪贴板架构、Unicode/ANSI 编码处理。 实用场景:自动化工具、开发调试、剪贴板历史记录等。 装 X 需求:在同事面前演示:"看,我一复制,程序就知道我抄了啥!"
  • Windows 提供了一套经典的"剪贴板查看器链"(Clipboard Viewer Chain)机制: 你可以把自己的窗口注册为"剪贴板查看器"。 一旦剪贴板内容发生变化,系统会向链中的每个窗口发送 WM_DRAWCLIPBOARD 消息。 你收到消息后,就可以打开剪贴板、读取内容、然后优雅地关掉它。 听起来是不是有点像"订阅-发布"模式?没错!只不过这是 1980 年代的 Windows 版 Pub/Sub。
  • 微软官方已不推荐使用 SetClipboardViewer(推荐用 AddClipboardFormatListener),但为了兼容性和教学目的,我们仍用经典方式。
  • 剪贴板可不是只存文本!它支持多种格式: 格式常量 含义 CF_TEXT ANSI 文本(通常是 GBK 编码) CF_UNICODETEXT UTF-16 文本(现代应用主流) CF_HDROP 文件拖放(比如从资源管理器复制文件) 其他自定义格式 比如 Word 的富文本、图片等 所以我们的程序必须: 枚举所有格式 优先读取 CF_UNICODETEXT 降级处理 CF_TEXT(并转码为 UTF-8) 识别文件拖放 对未知格式友好提示
  • 我们使用 golang.org/x/sys/windows 调用 Windows API,并配合 unsafe 和 reflect 直接操作内存(别怕,有安全兜底)。 下面就是完整源码(已加详细注释): package main import ( “bytes” “fmt” “io” “reflect” “syscall” “unicode/utf16” “unsafe” “golang.org/x/sys/windows” “golang.org/x/text/encoding/simplifiedchinese” “golang.org/x/text/transform” ) // ===== Win32 API Constants ===== const ( WM_CREATE = 0x0001 WM_DESTROY = 0x0002 WM_DRAWCLIPBOARD = 0x0308 WM_CHANGECBCHAIN = 0x030D CF_TEXT = 1 CF_BITMAP = 2 CF_UNICODETEXT = 13 CF_HDROP = 15 ) // ===== Win32 Structures ===== type WNDCLASSEX struct { Size uint32 Style uint32 WndProc uintptr ClsExtra int32 WndExtra int32 Instance syscall.Handle Icon syscall.Handle Cursor syscall.Handle Background uintptr MenuName *uint16 ClassName *uint16 IconSm syscall.Handle } type MSG struct { HWnd uintptr Message uint32 WParam uintptr LParam uintptr DwTime uint32 PtX int32 PtY int32 } // ===== Win32 API Procs ===== var ( user32 = windows.NewLazySystemDLL(“user32.dll”) kernel32 = windows.NewLazySystemDLL(“kernel32.dll”) shell32 = windows.NewLazySystemDLL(“shell32.dll”) procRegisterClassEx = user32.NewProc(“RegisterClassExW”) procCreateWindowEx = user32.NewProc(“CreateWindowExW”) procDefWindowProc = user32.NewProc(“DefWindowProcW”) procOpenClipboard = user32.NewProc(“OpenClipboard”) procCloseClipboard = user32.NewProc(“CloseClipboard”) procEnumClipboardFormats = user32.NewProc(“EnumClipboardFormats”) procGetClipboardData = user32.NewProc(“GetClipboardData”) procSetClipboardViewer = user32.NewProc(“SetClipboardViewer”) procGetMessage = user32.NewProc(“GetMessageW”) procTranslateMessage = user32.NewProc(“TranslateMessage”) procDispatchMessage = user32.NewProc(“DispatchMessageW”) procShowWindow = user32.NewProc(“ShowWindow”) procGlobalLock = kernel32.NewProc(“GlobalLock”) procGlobalUnlock = kernel32.NewProc(“GlobalUnlock”) procDragQueryFile = shell32.NewProc(“DragQueryFileW”) ) // ===== 窗口过程函数 ===== func windowProc(hwnd syscall.Handle, msg uint32, wParam uintptr, lParam uintptr) uintptr { switch msg { case WM_CREATE: _, _, _ = procSetClipboardViewer.Call(uintptr(hwnd)) case WM_DRAWCLIPBOARD: if ret, _, _ := procOpenClipboard.Call(uintptr(hwnd)); ret == 0 { fmt.Println(“❌ 无法打开剪切板”) break } var format uint32 = 0 var finalText string for { r1, _, _ := procEnumClipboardFormats.Call(uintptr(format)) format = uint32(r1) if format == 0 { break } hMem, _, _ := procGetClipboardData.Call(uintptr(format)) if hMem == 0 { continue } switch format { case CF_UNICODETEXT: text, err := getUnicodeText(hMem) if err == nil { finalText = text goto done } case CF_TEXT: text, err := getAnsiText(hMem) if err == nil && finalText == “” { finalText = text } case CF_HDROP: finalText = “” files, err := getHDropFiles(hMem) if err != nil { fmt.Println(“❌ 文件列表: 读取文件失败:”, err) } else { fmt.Printf(“📎 拖放的文件 (%d):n”, len(files)) for _, f := range files { fmt.Println(” -“, f) } } default: finalText = “” fmt.Printf(“📎 %sn”, getClipboardFormatName(format)) } } done: if finalText != “” { fmt.Printf(“📝 文本内容:n%sn”, finalText) } procCloseClipboard.Call() case WM_CHANGECBCHAIN: fmt.Println(“⛓️ 剪切板查看器链已更改”) default: ret, _, _ := procDefWindowProc.Call(uintptr(hwnd), uintptr(msg), wParam, lParam) return ret } return 0 } // ===== 辅助函数 ===== func getAnsiText(hMem uintptr) (string, error) { lpLock, _, _ := procGlobalLock.Call(hMem) if lpLock == 0 { return “”, fmt.Errorf(“无法锁定内存”) } defer procGlobalUnlock.Call(hMem) var size int for { b := *(*byte)(unsafe.Pointer(lpLock + uintptr(size))) if b == 0 { break } size++ } data := make([]byte, size) for i := 0; i < size; i++ { data[i] = *(*byte)(unsafe.Pointer(lpLock + uintptr(i))) } decoder := simplifiedchinese.GBK.NewDecoder() reader := transform.NewReader(bytes.NewReader(data), decoder) utf8Bytes, err := io.ReadAll(reader) if err != nil { return “”, err } return string(utf8Bytes), nil } func getUnicodeText(hMem uintptr) (string, error) { lpLock, _, _ := procGlobalLock.Call(hMem) if lpLock == 0 { return “”, fmt.Errorf(“无法锁定内存”) } defer procGlobalUnlock.Call(hMem) var length int for { w := *(*uint16)(unsafe.Pointer(lpLock + uintptr(length*2))) if w == 0 { break } length++ } sliceHeader := reflect.SliceHeader{ Data: lpLock, Len: length, Cap: length, } utf16Data := *(*[]uint16)(unsafe.Pointer(&sliceHeader)) runes := utf16.Decode(utf16Data) return string(runes), nil } func getHDropFiles(hMem uintptr) ([]string, error) { lpLock, _, _ := procGlobalLock.Call(hMem) if lpLock == 0 { return nil, fmt.Errorf(“无法锁定HDROP内存”) } defer procGlobalUnlock.Call(hMem) count, _, _ := procDragQueryFile.Call(lpLock, 0xFFFFFFFF, 0, 0) if count == 0 { return nil, fmt.Errorf(“HDROP中未找到文件”) } files := make([]string, count) for i := uintptr(0); i < count; i++ { var buf [260]uint16 _, _, _ = procDragQueryFile.Call(lpLock, i, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) files[i] = syscall.UTF16ToString(buf[:]) } return files, nil } func getClipboardFormatName(format uint32) string { if format <= 0xC000 { switch format { case CF_TEXT: return “CF_TEXT (ANSI文本)” case CF_UNICODETEXT: return “CF_UNICODETEXT” case CF_BITMAP: return “CF_BITMAP (位图)” case CF_HDROP: return “CF_HDROP (文件列表)” default: return “标准格式” } } buf := make([]uint16, 256) r1, _, _ := user32.NewProc(“GetClipboardFormatNameW”).Call( uintptr(format), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), ) if r1 == 0 { return fmt.Sprintf(“未知自定义格式 (%d)”, format) } return “自定义格式: ” + syscall.UTF16ToString(buf) } func init() { if err := shell32.Load(); err != nil { panic(“无法加载shell32.dll: ” + err.Error()) } } func main() { className := syscall.StringToUTF16Ptr(“ClipboardMonitorClass”) var wc WNDCLASSEX wc.Size = uint32(unsafe.Sizeof(wc)) wc.WndProc = syscall.NewCallback(windowProc) wc.Instance = 0 wc.ClassName = className wc.Style = 0x0002 // CS_HREDRAW ret, _, err := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc))) if ret == 0 { panic(“注册窗口类失败: ” + err.Error()) } hwnd, _, err := procCreateWindowEx.Call( 0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(“剪切板监视器”))), 0, 0, 0, 100, 100, 0, 0, 0, 0, ) if hwnd == 0 { panic(“创建窗口失败: ” + err.Error()) } procShowWindow.Call(hwnd, 0) // 隐藏窗口 fmt.Println(“🚀 剪贴板监视器已启动。现在可以复制内容进行测试!”) var msg MSG for { ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0) if ret <= 0 { break } procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg))) procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg))) } }
  • 本程序仅用于学习和本地调试。 实际产品中应避免滥用剪贴板监控(涉及隐私!)。 在企业环境中,此类行为可能违反安全策略。
  • 通过几十行 Go 代码,我们撬动了 Windows 底层的消息系统,实现了对剪贴板的"温柔窥探"。这不仅是一次技术实践,更是一次穿越回 Win32 时代的浪漫冒险。 下次当你复制密码时,记得看看控制台——说不定你的程序正在偷笑 。 以上就是基于Go编写一个Windows剪贴板监控器的详细内容,更多关于Go Windows剪贴板监控器的资料请关注风君子博客其它相关文章! 您可能感兴趣的文章: 基于Go语言开发篇一个命令行进程监控工具 详解如何使用Go语言进行文件监控和通知 Go语言性能监控和调优的工具和方法 golang调试bug及性能监控方式实践总结 GoLang日志监控系统实现
  • 目录
    • 为什么要做这个
    •  技术原理:Windows 剪贴板监听机制
    • 注意事项
    • 核心挑战:剪贴板数据格式
    • 代码实现:Go + Windows API
    • 如何运行
      • 安装依赖
      • 运行程序
      • 测试效果
    • 安全提醒
      • 结语

        你有没有想过,当你按下 Ctrl+C 的那一瞬间,你的剪贴板内容其实可以被"悄悄"记录下来?别慌,这不是什么黑客教程,而是一次用 Go 语言 和 Windows API 玩转系统底层的趣味实验!

        今天,我们就来手把手实现一个 剪贴板监视器 —— 它会在后台默默监听你的复制行为,并把内容打印出来(当然,也可以做更多事情,比如自动保存、翻译、甚至发到邮箱里 )。

        • 学习目的:理解 Windows 消息机制、剪贴板架构、Unicode/ANSI 编码处理。
        • 实用场景:自动化工具、开发调试、剪贴板历史记录等。
        • 装 X 需求:在同事面前演示:"看,我一复制,程序就知道我抄了啥!"

        Windows 提供了一套经典的"剪贴板查看器链"(Clipboard Viewer Chain)机制:

        • 你可以把自己的窗口注册为"剪贴板查看器"。
        • 一旦剪贴板内容发生变化,系统会向链中的每个窗口发送 WM_DRAWCLIPBOARD 消息。
        • 你收到消息后,就可以打开剪贴板、读取内容、然后优雅地关掉它。

        听起来是不是有点像"订阅-发布"模式?没错!只不过这是 1980 年代的 Windows 版 Pub/Sub。

        微软官方已不推荐使用 SetClipboardViewer(推荐用 AddClipboardFormatListener),但为了兼容性和教学目的,我们仍用经典方式。

        剪贴板可不是只存文本!它支持多种格式:

        格式常量 含义
        CF_TEXT ANSI 文本(通常是 GBK 编码)
        CF_UNICODETEXT UTF-16 文本(现代应用主流)
        CF_HDROP 文件拖放(比如从资源管理器复制文件)
        其他自定义格式 比如 Word 的富文本、图片等

        所以我们的程序必须:

        • 枚举所有格式
        • 优先读取 CF_UNICODETEXT
        • 降级处理 CF_TEXT(并转码为 UTF-8)
        • 识别文件拖放
        • 对未知格式友好提示

        我们使用 golang.org/x/sys/windows 调用 Windows API,并配合 unsafe 和 reflect 直接操作内存(别怕,有安全兜底)。

        下面就是完整源码(已加详细注释):

        package main
        
        import (
        	"bytes"
        	"fmt"
        	"io"
        	"reflect"
        	"syscall"
        	"unicode/utf16"
        	"unsafe"
        
        	"golang.org/x/sys/windows"
        	"golang.org/x/text/encoding/simplifiedchinese"
        	"golang.org/x/text/transform"
        )
        
        // ===== Win32 API Constants =====
        const (
        	WM_CREATE        = 0x0001
        	WM_DESTROY       = 0x0002
        	WM_DRAWCLIPBOARD = 0x0308
        	WM_CHANGECBCHAIN = 0x030D
        	CF_TEXT          = 1
        	CF_BITMAP        = 2
        	CF_UNICODETEXT   = 13
        	CF_HDROP         = 15
        )
        
        // ===== Win32 Structures =====
        type WNDCLASSEX struct {
        	Size       uint32
        	Style      uint32
        	WndProc    uintptr
        	ClsExtra   int32
        	WndExtra   int32
        	Instance   syscall.Handle
        	Icon       syscall.Handle
        	Cursor     syscall.Handle
        	Background uintptr
        	MenuName   *uint16
        	ClassName  *uint16
        	IconSm     syscall.Handle
        }
        
        type MSG struct {
        	HWnd    uintptr
        	Message uint32
        	WParam  uintptr
        	LParam  uintptr
        	DwTime  uint32
        	PtX     int32
        	PtY     int32
        }
        
        // ===== Win32 API Procs =====
        var (
        	user32   = windows.NewLazySystemDLL("user32.dll")
        	kernel32 = windows.NewLazySystemDLL("kernel32.dll")
        	shell32  = windows.NewLazySystemDLL("shell32.dll")
        
        	procRegisterClassEx      = user32.NewProc("RegisterClassExW")
        	procCreateWindowEx       = user32.NewProc("CreateWindowExW")
        	procDefWindowProc        = user32.NewProc("DefWindowProcW")
        	procOpenClipboard        = user32.NewProc("OpenClipboard")
        	procCloseClipboard       = user32.NewProc("CloseClipboard")
        	procEnumClipboardFormats = user32.NewProc("EnumClipboardFormats")
        	procGetClipboardData     = user32.NewProc("GetClipboardData")
        	procSetClipboardViewer   = user32.NewProc("SetClipboardViewer")
        	procGetMessage           = user32.NewProc("GetMessageW")
        	procTranslateMessage     = user32.NewProc("TranslateMessage")
        	procDispatchMessage      = user32.NewProc("DispatchMessageW")
        	procShowWindow           = user32.NewProc("ShowWindow")
        	procGlobalLock           = kernel32.NewProc("GlobalLock")
        	procGlobalUnlock         = kernel32.NewProc("GlobalUnlock")
        	procDragQueryFile        = shell32.NewProc("DragQueryFileW")
        )
        
        // ===== 窗口过程函数 =====
        func windowProc(hwnd syscall.Handle, msg uint32, wParam uintptr, lParam uintptr) uintptr {
        	switch msg {
        	case WM_CREATE:
        		_, _, _ = procSetClipboardViewer.Call(uintptr(hwnd))
        
        	case WM_DRAWCLIPBOARD:
        		if ret, _, _ := procOpenClipboard.Call(uintptr(hwnd)); ret == 0 {
        			fmt.Println("❌ 无法打开剪切板")
        			break
        		}
        		var format uint32 = 0
        		var finalText string
        
        		for {
        			r1, _, _ := procEnumClipboardFormats.Call(uintptr(format))
        			format = uint32(r1)
        			if format == 0 {
        				break
        			}
        
        			hMem, _, _ := procGetClipboardData.Call(uintptr(format))
        			if hMem == 0 {
        				continue
        			}
        
        			switch format {
        			case CF_UNICODETEXT:
        				text, err := getUnicodeText(hMem)
        				if err == nil {
        					finalText = text
        					goto done
        				}
        			case CF_TEXT:
        				text, err := getAnsiText(hMem)
        				if err == nil && finalText == "" {
        					finalText = text
        				}
        			case CF_HDROP:
        				finalText = ""
        				files, err := getHDropFiles(hMem)
        				if err != nil {
        					fmt.Println("❌ 文件列表: 读取文件失败:", err)
        				} else {
        					fmt.Printf("📎 拖放的文件 (%d):n", len(files))
        					for _, f := range files {
        						fmt.Println(" -", f)
        					}
        				}
        			default:
        				finalText = ""
        				fmt.Printf("📎 %sn", getClipboardFormatName(format))
        			}
        		}
        	done:
        		if finalText != "" {
        			fmt.Printf("📝 文本内容:n%sn", finalText)
        		}
        		procCloseClipboard.Call()
        
        	case WM_CHANGECBCHAIN:
        		fmt.Println("⛓️ 剪切板查看器链已更改")
        
        	default:
        		ret, _, _ := procDefWindowProc.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
        		return ret
        	}
        	return 0
        }
        
        // ===== 辅助函数 =====
        func getAnsiText(hMem uintptr) (string, error) {
        	lpLock, _, _ := procGlobalLock.Call(hMem)
        	if lpLock == 0 {
        		return "", fmt.Errorf("无法锁定内存")
        	}
        	defer procGlobalUnlock.Call(hMem)
        
        	var size int
        	for {
        		b := *(*byte)(unsafe.Pointer(lpLock + uintptr(size)))
        		if b == 0 {
        			break
        		}
        		size++
        	}
        
        	data := make([]byte, size)
        	for i := 0; i < size; i++ {
        		data[i] = *(*byte)(unsafe.Pointer(lpLock + uintptr(i)))
        	}
        
        	decoder := simplifiedchinese.GBK.NewDecoder()
        	reader := transform.NewReader(bytes.NewReader(data), decoder)
        	utf8Bytes, err := io.ReadAll(reader)
        	if err != nil {
        		return "", err
        	}
        	return string(utf8Bytes), nil
        }
        
        func getUnicodeText(hMem uintptr) (string, error) {
        	lpLock, _, _ := procGlobalLock.Call(hMem)
        	if lpLock == 0 {
        		return "", fmt.Errorf("无法锁定内存")
        	}
        	defer procGlobalUnlock.Call(hMem)
        
        	var length int
        	for {
        		w := *(*uint16)(unsafe.Pointer(lpLock + uintptr(length*2)))
        		if w == 0 {
        			break
        		}
        		length++
        	}
        
        	sliceHeader := reflect.SliceHeader{
        		Data: lpLock,
        		Len:  length,
        		Cap:  length,
        	}
        	utf16Data := *(*[]uint16)(unsafe.Pointer(&sliceHeader))
        
        	runes := utf16.Decode(utf16Data)
        	return string(runes), nil
        }
        
        func getHDropFiles(hMem uintptr) ([]string, error) {
        	lpLock, _, _ := procGlobalLock.Call(hMem)
        	if lpLock == 0 {
        		return nil, fmt.Errorf("无法锁定HDROP内存")
        	}
        	defer procGlobalUnlock.Call(hMem)
        
        	count, _, _ := procDragQueryFile.Call(lpLock, 0xFFFFFFFF, 0, 0)
        	if count == 0 {
        		return nil, fmt.Errorf("HDROP中未找到文件")
        	}
        
        	files := make([]string, count)
        	for i := uintptr(0); i < count; i++ {
        		var buf [260]uint16
        		_, _, _ = procDragQueryFile.Call(lpLock, i, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
        		files[i] = syscall.UTF16ToString(buf[:])
        	}
        	return files, nil
        }
        
        func getClipboardFormatName(format uint32) string {
        	if format <= 0xC000 {
        		switch format {
        		case CF_TEXT:
        			return "CF_TEXT (ANSI文本)"
        		case CF_UNICODETEXT:
        			return "CF_UNICODETEXT"
        		case CF_BITMAP:
        			return "CF_BITMAP (位图)"
        		case CF_HDROP:
        			return "CF_HDROP (文件列表)"
        		default:
        			return "标准格式"
        		}
        	}
        	buf := make([]uint16, 256)
        	r1, _, _ := user32.NewProc("GetClipboardFormatNameW").Call(
        		uintptr(format),
        		uintptr(unsafe.Pointer(&buf[0])),
        		uintptr(len(buf)),
        	)
        	if r1 == 0 {
        		return fmt.Sprintf("未知自定义格式 (%d)", format)
        	}
        	return "自定义格式: " + syscall.UTF16ToString(buf)
        }
        
        func init() {
        	if err := shell32.Load(); err != nil {
        		panic("无法加载shell32.dll: " + err.Error())
        	}
        }
        
        func main() {
        	className := syscall.StringToUTF16Ptr("ClipboardMonitorClass")
        
        	var wc WNDCLASSEX
        	wc.Size = uint32(unsafe.Sizeof(wc))
        	wc.WndProc = syscall.NewCallback(windowProc)
        	wc.Instance = 0
        	wc.ClassName = className
        	wc.Style = 0x0002 // CS_HREDRAW
        
        	ret, _, err := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
        	if ret == 0 {
        		panic("注册窗口类失败: " + err.Error())
        	}
        
        	hwnd, _, err := procCreateWindowEx.Call(
        		0,
        		uintptr(unsafe.Pointer(className)),
        		uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("剪切板监视器"))),
        		0,
        		0, 0, 100, 100,
        		0, 0, 0, 0,
        	)
        	if hwnd == 0 {
        		panic("创建窗口失败: " + err.Error())
        	}
        
        	procShowWindow.Call(hwnd, 0) // 隐藏窗口
        
        	fmt.Println("🚀 剪贴板监视器已启动。现在可以复制内容进行测试!")
        
        	var msg MSG
        	for {
        		ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
        		if ret <= 0 {
        			break
        		}
        		procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
        		procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
        	}
        }
        

        go mod init clipboard-monitor
        go get golang.org/x/sys/windows
        go get golang.org/x/text/encoding/simplifiedchinese
        

        保存为 main.go,然后:

        go run main.go
        

        复制一段文字、一张图片、或几个文件试试! 你会看到类似这样的输出:

        🚀 剪贴板监视器已启动。现在可以复制内容进行测试!
        📝 文本内容:
        Hello, 剪贴板!
        📎 拖放的文件 (2):
         – C:UsersAlicePicturescat.jpg
         – C:UsersAliceDocumentsreport.pdf
        📎 CF_BITMAP (位图)

        • 本程序仅用于学习和本地调试。
        • 实际产品中应避免滥用剪贴板监控(涉及隐私!)。
        • 在企业环境中,此类行为可能违反安全策略。

        通过几十行 Go 代码,我们撬动了 Windows 底层的消息系统,实现了对剪贴板的"温柔窥探"。这不仅是一次技术实践,更是一次穿越回 Win32 时代的浪漫冒险。

        下次当你复制密码时,记得看看控制台——说不定你的程序正在偷笑 。

        以上就是基于Go编写一个Windows剪贴板监控器的详细内容,更多关于Go Windows剪贴板监控器的资料请关注风君子博客其它相关文章!

        您可能感兴趣的文章:

        • 基于Go语言开发篇一个命令行进程监控工具
        • 详解如何使用Go语言进行文件监控和通知
        • Go语言性能监控和调优的工具和方法
        • golang调试bug及性能监控方式实践总结
        • GoLang日志监控系统实现

        站内搜索