Skip to content

Go SDK

将 IconHash 的哈希与指纹能力直接嵌入你的 Go 程序,无需启动外部服务。

v1.x 稳定 Go 1.21+ API 冻结

为什么要用 SDK 而不是 CLI?

当你需要在已有 Go 服务里按 URL 实时识别组件、做资产测绘、或对一批图标做批量去重时,反复拉起 CLI 进程的代价过高。SDK 把 pkg/hasherpkg/fingerprint 直接暴露给你:一次 import,零额外进程,还能复用 context.Context 做超时与取消。

包结构

import 路径

go
import (
    "github.com/cyberspacesec/iconhash-skills/pkg/hasher"
    "github.com/cyberspacesec/iconhash-skills/pkg/fingerprint"
)

该用哪个函数?

决策口诀

  • 只要哈希值Hash* 系列,返回 string,轻量、零依赖。
  • 识别出是哪个组件/服务Identify* 系列,必须传 *fingerprint.DB
  • 想一次拿全(发现图标→哈希→匹配)→ 直接 IdentifySite,等价于 CLI 的 identify 子命令。

核心 API 速查

速查表(可折叠)
函数 / 方法入参返回备注
hasherNew(opts)*HashOptions*IconHasher构造器,opts==nil 用默认
hasherDefaultOptions()*HashOptions超时 10s、UA 伪 Chrome
hasherNewOptionsWithProxy(url, timeout, insecure)string, time.Duration, bool*HashOptions, error一键配代理
*IconHasherHashFromURLctx, url(string, error)网络来源
*IconHasherHashFromFilepath(string, error)本地文件
*IconHasherHashFromBase64b64(string, error)base64 字符串
*IconHasherHashFromBytes[]byte(string, error)内存字节
*IconHasherHashFromReaderio.Reader(string, error)流式
*IconHasherDiscoverFaviconctx, siteURL, opts([]string, error)仅发现 URL
*IconHasherDiscoverAndHashctx, siteURL, opts[]DiscoverResult发现+哈希
*IconHasherIdentifyctx, siteURL, db, opts[]IdentifyResult端到端
fingerprintDefaultDB()*DB内置/外部自动选
fingerprintNewDB(entries)[]FingerprintEntry*DB自定义构造
fingerprintNewDBFromJSON(path)string(*DB, error)文件加载
fingerprintNewDBFromData(data)[]byte(*DB, error)字节加载
*DBLookupstring[]FingerprintEntry精确匹配,未命中返回 nil slice
*DBSearchquery[]FingerprintEntry模糊搜索
*DBAddEntry / RemoveEntryFingerprintEntryerror / int运行时增删(写锁)

核心 API 签名与语义

稳定 v1.x 冻结

以下签名取自源码 pkg/hasher/hasher.gopkg/fingerprint/fingerprint.go方法均挂在 *IconHasher / *DB,包级不存在同名函数——文档前述的 hasher.HashURL(...) 等是简写,实际写法是 h := hasher.New(nil); h.HashFromURL(...)

哈希系列

go
// 所有哈希方法均为 *IconHasher 的方法,返回 (string, error)
func (h *IconHasher) HashFromURL(ctx context.Context, url string) (string, error)
func (h *IconHasher) HashFromFile(filePath string) (string, error)
func (h *IconHasher) HashFromBase64(base64Data string) (string, error)
func (h *IconHasher) HashFromBytes(data []byte) (string, error)
func (h *IconHasher) HashFromReader(r io.Reader) (string, error)

// 每个 Hash* 都有 *WithOption 变体,按调用覆盖 UseUint32 而不改共享状态
func (h *IconHasher) HashFromURLWithOption(ctx context.Context, url string, useUint32 bool) (string, error)

参数说明

参数类型含义必填
ctxcontext.Context超时/取消传播,仅 *FromURL 系列强相关是(URL 系)
url / filePath / base64Datastring数据来源定位符
data[]byte已解码的图标字节
rio.Reader任意流(stdin、管道、网络流)
useUint32bool单次返回 uint32 语义,true 时返回值无符号
返回值结构
  • 第一个返回值 string:MurmurHash3 的字符串表示(默认带符号,如 "-1161367058"useUint32=true 时为无符号,如 "1161848134")。
  • 第二个返回值 error:失败时非 nil,可用 errors.Is 匹配哨兵错误(见错误处理)。
  • 注意:成功时第一个值必为非空字符串;失败时为 ""。不要用 == "" 判错,要检查 err != nil

使用场景:HashFromURL vs HashFromFile vs HashFromReader

  • HashFromURL:图标托管在公网/内网 HTTP 服务上,需要 SDK 内部下载。适合在线资产测绘。注意要传 ctx 控制超时,避免长尾请求拖垮调用方。
  • HashFromFile:图标已落盘(爬虫产物、镜像解包、CI 缓存)。离线批处理首选,零网络依赖、可重复。
  • HashFromReader:数据来源是流式且不想全量驻留内存(stdin 管道、tar 解包边读边算、http.Response.Body 直接消费)。内存敏感无法 seek 的场景必选。
  • 简记:在线→URL,离线→File,流式→Reader

常见误用

  • 忘检查 errorh, _ := h.HashFromURL(ctx, url); db.Lookup(h)——网络失败时 h == ""Lookup("") 永远返回空,静默漏报。务必 if err != nil { ... }
  • ctx 透传缺失HashFromURL(context.Background(), ...) 在批量场景会失去上层超时控制。应传业务 ctx,让父超时能级联取消。
  • 复用 *IconHasher 跨 goroutine 改 OptionsOptions() 返回的是副本,改副本不影响实例;但要改实例配置应重新 New,不要在并发中修改。
  • HashFromBase64 当 URL 用HashFromBase64("https://...") 不会下载,会把整段 URL 当 base64 解码并报 ErrInvalidBase64

发现与识别系列

go
// 发现:返回站点上所有 favicon 的 URL
func (h *IconHasher) DiscoverFavicon(ctx context.Context, siteURL string, opts *DiscoverFaviconOptions) ([]string, error)

// 发现 + 哈希:等价 CLI `iconhash discover`
func (h *IconHasher) DiscoverAndHash(ctx context.Context, siteURL string, opts *DiscoverFaviconOptions) []DiscoverResult

// 端到端:发现 + 哈希 + 查库,等价 CLI `iconhash identify`
func (h *IconHasher) Identify(ctx context.Context, siteURL string, db *fingerprint.DB, opts *DiscoverFaviconOptions) []IdentifyResult

// 直接给图标 URL/文件/base64/字节 的识别变体
func (h *IconHasher) IdentifyIconURL(ctx context.Context, iconURL string, db *fingerprint.DB) (IconIdentifyResult, error)
func (h *IconHasher) IdentifyIconFile(path string, db *fingerprint.DB) (IconIdentifyResult, error)
func (h *IconHasher) IdentifyIconBase64(data string, db *fingerprint.DB) (IconIdentifyResult, error)
func (h *IconHasher) IdentifyIconBytes(data []byte, db *fingerprint.DB) (IconIdentifyResult, error)

结果类型

go
type DiscoverResult struct {
    URL  string  // 发现到的 favicon URL
    Hash string  // MMH3 哈希(出错时为空)
    Err  error   // 该 URL 失败时的错误,逐条独立
}

type IdentifyResult struct {
    IconURL     string                    // 来源 URL
    Hash        string                    // 哈希
    Fingerprint []fingerprint.FingerprintEntry // 命中的指纹(可能多条,未命中为空 slice)
}

type IconIdentifyResult struct {
    Hash        string
    Fingerprint []fingerprint.FingerprintEntry
}
返回值结构
  • DiscoverAndHash / Identify 不返回 error——它们是"尽力而为"的批量接口,逐条结果里的 ErrDiscoverResult)或空 FingerprintIdentifyResult)承载失败信息。这是为了避免单个图标失败导致整批丢失。
  • IdentifyIcon* 系列返回 (IconIdentifyResult, error)——因为输入是单一图标,失败应显式上报,故用 error 而非空 slice。
  • Matches 字段是 slice[]FingerprintEntry):同一个哈希可能对应多个组件(如某通用图标被多服务复用),不要假定只命中一条。

使用场景:Identify vs IdentifyIconURL vs DiscoverAndHash

  • Identify:从站点 URL 出发,端到端拿"组件识别结果"。资产测绘主入口
  • IdentifyIconURL:你已知道图标直链(如 https://x.com/favicon.ico),只差最后一步匹配。跳过发现阶段,更快。
  • DiscoverAndHash:只要哈希、不要指纹匹配(例如自建指纹库、做图标去重)。中间产物,比 Identify 少一次 Lookup
  • 简记:要结果→Identify,有直链→IdentifyIconURL,只要哈希→DiscoverAndHash

常见误用

  • Identify 当哈希函数Identify 内部会发起多次 HTTP(发现阶段要抓 HTML 试探 /favicon.ico 等路径),开销远大于 HashFromURL。只想要哈希时别用它。
  • 忽略 DiscoverResult.Err:批量场景里某条 Err != nil 是常态(某图标 404),应统计而非整批 abort。
  • optsnilDiscoverFavicon 接受 nil(用默认),但生产建议显式传 DefaultDiscoverOptions() 并按需调整,避免版本升级后默认行为变化。
  • dbnilIdentify 系列要求 db != nil,传 nil 会 panic。用 fingerprint.DefaultDB() 兜底。

指纹库系列

go
// 构造
func NewDB(entries []FingerprintEntry) *DB
func NewDBFromJSON(path string) (*DB, error)
func NewDBFromData(data []byte) (*DB, error)
func DefaultDB() *DB

// 查询(只读,并发安全)
func (db *DB) Lookup(hash string) []FingerprintEntry
func (db *DB) LookupByCategory(category string) []FingerprintEntry
func (db *DB) LookupByTag(tag string) []FingerprintEntry
func (db *DB) Search(query string) []FingerprintEntry
func (db *DB) All() []FingerprintEntry
func (db *DB) Count() int
func (db *DB) TotalEntries() int
func (db *DB) Categories() map[string]int

// 增删(写锁,不能与并发查询同时进行 —— 见并发安全图)
func (db *DB) AddEntry(e FingerprintEntry) error
func (db *DB) RemoveEntry(e FingerprintEntry) int
func (db *DB) RemoveByHash(hash string) int
func (db *DB) RemoveByService(service string) int
func (db *DB) LoadFromJSON(path string) error
func (db *DB) LoadFromData(data []byte) error

// 导出
func (db *DB) ExportJSON() ([]byte, error)
func (db *DB) ExportCSV() string
func (db *DB) ExportJSONToFile(path string) error
func (db *DB) ExportCSVToFile(path string) error
go
type FingerprintEntry struct {
    Hash        string   `json:"hash"`                  // MMH3 哈希(字符串形式)
    Service     string   `json:"service"`               // 组件名
    Category    string   `json:"category,omitempty"`    // 分类
    Version     string   `json:"version,omitempty"`     // 版本
    Description string   `json:"description,omitempty"` // 描述
    Tags        []string `json:"tags,omitempty"`        // 标签
    Reference   string   `json:"reference,omitempty"`   // 参考链接
    IconURL     string   `json:"icon_url,omitempty"`    // 已知图标 URL
    Source      string   `json:"source,omitempty"`      // 数据来源
}

Lookup 参数说明

参数类型含义
hashstringHashFrom* 返回的字符串哈希;与 FingerprintEntry.Hash 字段类型一致
返回值结构
  • 返回 []FingerprintEntry未命中时返回 nil(长度为 0)。
  • 同一哈希可能命中多条(不同服务共用同一图标),调用方应遍历而非取首条。
  • 返回的 slice 是内部索引的副本,修改它不会影响 DB;但修改其中的 FingerprintEntry 值类型字段是安全的(值拷贝)。

使用场景:DefaultDB vs NewDBFromJSON vs NewDB+LoadFromJSON

  • DefaultDB():开箱即用,覆盖 700+ 常见组件。绝大多数场景的首选。Full 构建零 IO,Lite 构建自动拉取缓存。
  • NewDBFromJSON(path):你有私有/自建指纹库(如内部资产梳理出的图标映射),完全替代内置库。
  • NewDB(nil) + db.LoadFromJSON(path):先拿空 DB,再增量合并多个来源(内置 + 自建 + 社区)。适合渐进式构建指纹库。
  • 简记:默认→DefaultDB,纯自建→NewDBFromJSON,多源合并→NewDB+Load*。

常见误用

  • Lookup 后忘判空db.Lookup(h)[0].Service——未命中时 panic。应 if es := db.Lookup(h); len(es) > 0 { ... }
  • 并发中调 AddEntryAddEntry 取写锁,与并发 Lookup(读锁)互斥;高频查询场景里夹一次写会阻塞所有读。冷启动期写完再并发读,运行期避免写。
  • Hash 字段当 int32FingerprintEntry.HashstringLookup 入参也是 string。从 HashFrom* 拿到的 string 可直接传入,无需类型转换。
  • NewDBFromJSON 不检查文件存在:路径错误会返回 error,不要 _ 忽略,否则拿到空 DB 后所有 Lookup 都返回空。

类型关系图

下图展示 HashOptions / IconHasher / DB / FingerprintEntry / IdentifyResult 等核心类型之间的构造与依赖关系(实线=构造/持有,虚线=使用/返回)。

类型记忆法

  • HashOptions 是配置(值语义,可拷贝)→ 喂给 NewIconHasher
  • IconHasher 是无状态服务(内部只读 options + httpClient)→ 可安全跨 goroutine 复用。
  • DB 是带读写锁的索引→ 只读查询并发安全,写操作需独占。
  • *Result / FingerprintEntry 是值类型→ 拿到即拷贝,可放心持有。

哈希计算

基础用法

稳定 入门

文档为可读性用 hasher.HashURL(...) 等简写,真实方法名是 HashFromURL 等挂在 *IconHasher 上。下方"完整可运行示例"使用真实签名。

go
package main

import (
    "context"
    "fmt"
    "github.com/cyberspacesec/iconhash-skills/pkg/hasher"
)

func main() {
    ctx := context.Background()

    // 从 URL
    h1, _ := hasher.HashURL(ctx, "https://example.com/favicon.ico")

    // 从文件
    h2, _ := hasher.HashFile("favicon.ico")

    // 从 base64
    h3, _ := hasher.HashBase64("AAABAA...")

    // 从字节
    h4, _ := hasher.HashBytes(rawBytes)

    fmt.Println(h1, h2, h3, h4)
}
完整可运行示例(含错误处理)
go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/cyberspacesec/iconhash-skills/pkg/hasher"
)

func main() {
    ctx := context.Background()

    h1, err := hasher.HashURL(ctx, "https://example.com/favicon.ico")
    if err != nil {
        log.Printf("HashURL 失败: %v", err)
    }

    h2, err := hasher.HashFile("favicon.ico")
    if err != nil {
        log.Printf("HashFile 失败: %v", err)
    }

    h3, err := hasher.HashBase64("AAABAA...")
    if err != nil {
        log.Printf("HashBase64 失败: %v", err)
    }

    fmt.Println("URL  ->", h1)
    fmt.Println("File ->", h2)
    fmt.Println("B64  ->", h3)
}

哈希为什么是 string 而不是 int32

IconHash 沿用 fofa 的 MurmurHash3 (int32) 表示,但对外统一返回字符串,原因是:int32 在不同语言/序列化器里符号位与字节序处理不一致,而字符串 "1161848134" / "-1161367058" 是无歧义的。HashOptions.Uint32 = true 可切回原始 uint32 语义,便于和某些第三方库对接。

调用链

高级选项(代理 / 超时)

稳定 生产必读

HashOptions 真实字段如下,下方示例对应源码 NewOptionsWithProxy

go
type HashOptions struct {
    UseUint32          bool          // 返回 uint32 语义
    RequestTimeout     time.Duration // HTTP 超时(默认 10s)
    InsecureSkipVerify bool          // 跳过 TLS 校验(默认 true,仅适合内网)
    UserAgent          string        // UA(默认伪 Chrome)
    HTTPClient         *http.Client  // 注入自定义 client(代理/连接池/TLS)
    MaxIconSize        int64         // 图标体积上限(默认 10MB,超出截断)
}

func NewOptionsWithProxy(proxyURL string, timeout time.Duration, insecureSkipVerify bool) (*HashOptions, error)

NewOptionsWithProxy 参数说明

参数类型含义推荐值
proxyURLstring支持 http:// / https:// / socks5://公司出口代理
timeouttime.DurationHTTP client 超时公网 5–10s,内网 3s
insecureSkipVerifybool是否跳过 TLS 校验公网 false,内网可 true
返回值结构
  • 成功返回 *HashOptions,其 HTTPClient 已绑定 http.ProxyURL(proxy)Transport
  • 失败返回 nil, error:仅当 proxyURL 无法 url.Parse 时(如协议不合法)。网络层错误在实际请求时才暴露,不会在此提前报错。
go
opts, _ := hasher.NewOptionsWithProxy(
    "http://127.0.0.1:8080", // 代理
    30 * time.Second,         // 超时
    true,                     // 跳过 TLS 校验
)
h := hasher.New(opts)
hash, _ := h.HashURL(ctx, "https://example.com/favicon.ico")

代理与 TLS 的取舍

  • InsecureSkipVerify = true 仅用于内网/测试,公网抓取务必保持 false,否则中间人可注入伪造图标污染哈希。
  • 代理建议走公司出口:很多组件官网对 IDC IP 段返回 403,导致 HashURL 拿到的是错误页哈希。
  • Timeout 默认值偏宽松(30s),生产环境建议按 P99 设置 5–10s,避免 goroutine 堆积。

指纹库

700+ 内置指纹 embed.FS

Full vs Lite 构建

  • Full 构建:指纹 JSON 通过 go:embed 编译进二进制,IsEmbedded() == true,单文件即用,体积约 +1MB。
  • Lite 构建:不内嵌指纹,首次调用时从本地缓存或远程仓库拉取,适合对二进制体积敏感的场景。
  • 调用 DefaultDB() 时无需关心模式,库内部会自动选路径——见下方流程图。

加载默认库

推荐 零配置
go
db := fingerprint.DefaultDB()

使用场景

  • 绝大多数情况直接用 DefaultDB():Full 构建读 embed.FS 零 IO,Lite 构建自动落缓存。它还会合并环境变量 ICONHASH_FINGERPRINT_DB 指向的库,便于不改代码切换数据源。
  • 仅当你有明确的私有库且不想让内置库污染结果时,才用 NewDBFromJSON 替代。

查询

稳定
go
db := fingerprint.DefaultDB()
result := db.Lookup(-1161367058)
if result != nil {
    fmt.Println(result.Service) // nginx
}

上例为旧式简写,真实签名是 Lookup(hash string) []FingerprintEntry

源码里 Lookup 入参为 string、返回 []FingerprintEntry(slice,非单指针)。正确写法:

go
h, err := h.HashFromURL(ctx, "https://example.com/favicon.ico")
if err != nil { return err }
entries := db.Lookup(h)              // h 已是 string
for _, e := range entries {          // 可能多条
    fmt.Println(e.Service, e.Category)
}

历史文档里的 db.Lookup(-1161367058) 返回 *FingerprintEntry 的写法仅作示意,不要照抄——会因类型不匹配编译失败。

使用场景

  • 精确匹配Lookup(hash)——O(1),用于"我已有哈希,查它是什么"。
  • 分类检索LookupByCategory("web-server")——做组件族统计。
  • 模糊搜索Search("nginx")——跨 service/category/tags/description 子串匹配,用于"我知道名字的一部分"。

常见误用

  • 忘了 nil/slice 判空Lookup 返回 nil slice 时 len==0,用 range 安全,但 [0] 会 panic。
  • SearchLookupSearch 是子串扫描、O(n);Lookup 是哈希表、O(1)。批量场景误用 Search 会显著拖慢。

从自定义文件加载

进阶
go
// 从 JSON 文件
db, _ := fingerprint.NewDBFromJSON("my-fingerprints.json")

// 从 JSON 字节
db, _ := fingerprint.NewDBFromData(jsonBytes)

使用场景

  • NewDBFromJSON(path):私有指纹库已落盘成 JSON,一次性加载为 *DB完全替代内置库,结果里不会混入内置指纹。
  • NewDBFromData(data):JSON 来自网络/API 响应/数据库 blob,未落盘。无 IO,适合动态构建。

常见误用

  • 忽略 errordb, _ := NewDBFromJSON(...) 在文件不存在或 JSON 非法时返回 nil,后续 db.Lookup 直接 nil panic。务必检查 err
  • JSON 格式错:必须是数组[{...}, {...}]),不是单对象、不是包了一层 {"fingerprints": [...]}FingerprintEntry.Hash字符串,写成数字会被解码失败。

合并内置与自定义

进阶 多源融合
自定义指纹 JSON 格式
json
[
  {
    "hash": -1161367058,
    "service": "nginx",
    "category": "web-server",
    "tags": ["proxy", "reverse-proxy"],
    "description": "Nginx 默认 favicon"
  },
  {
    "hash": 1135459180,
    "service": "Apache HTTPD",
    "category": "web-server",
    "tags": ["proxy"],
    "description": "Apache 默认 favicon"
  }
]

字段说明:

  • hashint32,必填;与 db.Lookup 入参类型一致。
  • service:组件/服务名,建议小写连字符。
  • category:自由分类,如 web-servercmsrouter
  • tagsdescription:可空,用于在 CLI/SDK 输出时补充上下文。

一键识别(推荐)

IdentifySite 是端到端封装,等价于 CLI 的 identify

推荐入口 端到端

为什么推荐它?

IdentifySite 一次调用即完成 发现图标 → 下载 → 哈希 → 查库 → 返回结构化结果,内部已处理好"一个站点多个图标"的情况(返回 []IdentifyResult)。绝大多数资产测绘场景用它就够了,省去手动拼装 DiscoverFavicons + HashURL + Lookup 的样板代码。

go
ctx := context.Background()
db := fingerprint.DefaultDB()

results := hasher.IdentifySite(ctx, "https://example.com", db)
for _, r := range results {
    fmt.Printf("图标: %s\n", r.IconURL)
    fmt.Printf("哈希: %s\n", r.Hash)
    fmt.Printf("服务: %s\n", r.Fingerprint.Service)
}

上例为包级简写,真实方法名是 (*IconHasher).Identify

源码里 Identify*IconHasher 的方法,签名:

go
func (h *IconHasher) Identify(ctx context.Context, siteURL string, db *fingerprint.DB, opts *DiscoverFaviconOptions) []IdentifyResult

正确写法:

go
h := hasher.New(nil)
db := fingerprint.DefaultDB()
results := h.Identify(ctx, "https://example.com", db, nil)
for _, r := range results {
    for _, e := range r.Fingerprint {  // Fingerprint 是 slice
        fmt.Printf("服务: %s\n", e.Service)
    }
}

注意两点:① 需要 h := hasher.New(nil) 拿到实例;② r.Fingerprint[]FingerprintEntry,要遍历而非 .Service

使用场景:何时该用 Identify

  • 资产测绘:扫一批站点 URL,要的是"这个站是什么组件"的标签。
  • 运维巡检:对比线上 favicon 哈希是否偏离已知基线(识别被篡改/仿冒)。
  • 不要用它做"单纯哈希":只要哈希走 DiscoverAndHashHashFromURLIdentify 多一次 Lookup,开销不必要。

识别函数矩阵

并发批量

并发安全 需自备信号量

SDK 本身是并发安全的,可直接用 goroutine 批量:

并发批量三个坑

  1. 不要无界 goroutine:URL 列表大时务必用信号量(如示例的 chan struct{})限制并发度,否则会瞬时打满 fd 与内存。
  2. 共享 *fingerprint.DB 无需加锁:内部是只读 map,多 goroutine 并发 Lookup 安全;但不要在并发过程中调用任何会改写 DB 的方法
  3. 结果收集要 sync.Mutex:示例里用 mu 保护 results slice 的 append,不要用 channel 替代后再关闭——顺序无法保证且易死锁。

DB 并发读安全,但写要独占

源码里 DB 内部是 sync.RWMutex + map[string][]FingerprintEntry并发 Lookup/Search(读锁)完全安全,无需调用方加锁。但 AddEntry/RemoveEntry/LoadFromJSON 等写方法取写锁,会阻塞所有读。生产实践:冷启动期把指纹库写满,运行期只读不写;如需运行期增量,建议单独维护一个"待合并"的临时 DB,定期整体替换指针。

并发安全时序

下图展示多个 worker goroutine 并发读 DB、以及一个写者尝试 AddEntry 时的锁竞争关系。

图中可见的陷阱

  • 写者 Lock() 期间,所有读者的 RLock() 也会排队(Go 的 sync.RWMutex 在有写者等待时会阻止新读者,避免写者饿死)。这意味着一次 AddEntry 可能让整批并发查询的尾延迟飙升。
  • 若你在批量 Identify 的 hot path 上调用 AddEntry,相当于在并发读中插入串行点,吞吐会被这个写锁"割喉"。移到批处理之外
go
func batchIdentify(urls []string, db *fingerprint.DB) []hasher.IdentifyResult {
    var (
        wg      sync.WaitGroup
        mu      sync.Mutex
        results []hasher.IdentifyResult
        sem     = make(chan struct{}, 10) // 并发度 10
    )

    for _, u := range urls {
        wg.Add(1)
        go func(url string) {
            defer wg.Done()
            sem <- struct{}{}
            defer func() { <-sem }()

            r := hasher.IdentifySite(ctx, url, db)
            mu.Lock()
            results = append(results, r...)
            mu.Unlock()
        }(u)
    }
    wg.Wait()
    return results
}

类型一览

速记图

下图为可读性速记,方法名与字段名有简化。真实字段与方法签名见 类型关系图

v2 兼容性预告

当前 Hash* 返回 stringLookup 入参为 int32 的设计将在 v1.x 全程保持。v2 计划统一为 Hash 单一返回类型并支持 Result 链式调用,届时会提供迁移脚本。如果你在产线大量调用 SDK,建议封装一层薄适配层,避免将来被动改签名。

错误处理速查

稳定 errors.Is

Hash*Identify* 都遵循 Go 惯例的 (value, error) 返回。下表是常见错误的处置:

错误场景触发函数典型 err建议
网络超时HashURL / IdentifySitecontext.DeadlineExceeded调小并发度、加代理、设短超时
非 2xx 响应HashURLunexpected status: 403多为 IP 被封,换出口或重试
文件不存在HashFileopen ...: no such file调用前 os.Stat 预检
base64 非法HashBase64illegal base64 data校验输入格式
指纹未命中Lookup(无 err,返回 nil)当作"未知",写入自有库

哨兵错误与 errors.Is

稳定 v1.x 引入

Hash* 系列返回结构化错误,可用 errors.Is 精确分支处理,不必字符串匹配:

go
var (
    ErrInvalidURL      = errors.New("invalid URL")
    ErrHTTPError       = errors.New("HTTP request failed")
    ErrInvalidBase64   = errors.New("invalid base64 data")
    ErrFileNotFound    = errors.New("file not found")
    ErrNetworkError    = errors.New("network error")
    ErrReaderFailed    = errors.New("reader failed")
    ErrNoFaviconFound  = errors.New("no favicon found")
)

// 提取 HTTP 状态码(非 HTTPError 返回 -1)
func HTTPStatusCode(err error) int

哨兵错误分类与处置

哨兵触发函数含义推荐处置
ErrInvalidURLHashFromURLURL 无法 url.Parse校验输入,不入重试
ErrHTTPErrorHashFromURL非 2xx 响应HTTPStatusCode(err) 取码;403/429 换出口,5xx 可重试
ErrNetworkErrorHashFromURL连接/DNS/TLS/超时指数退避重试,检查代理
ErrInvalidBase64HashFromBase64base64 解码失败校验输入,不重试
ErrFileNotFoundHashFromFile文件不存在/不可读os.Stat 预检,不重试
ErrReaderFailedHashFromReaderio.Reader.Read 失败检查上游流,重试需可重放
ErrNoFaviconFoundDiscoverFavicon站点无 favicon当作"无图标",不重试
errors.Is 用法示例
go
h, err := hasher.New(nil).HashFromURL(ctx, url)
switch {
case err == nil:
    // 成功
case errors.Is(err, hasher.ErrHTTPError):
    code := hasher.HTTPStatusCode(err)
    if code == 403 || code == 429 {
        // 换代理/退避
    } else if code >= 500 {
        // 服务端错误,可重试
    }
case errors.Is(err, hasher.ErrNetworkError):
    // 指数退避
case errors.Is(err, hasher.ErrInvalidURL):
    // 输入问题,不重试,记录后跳过
default:
    // 其他未知错误
}

使用场景

  • 批量脚本:对 ErrHTTPError(403) 走"换代理重试",对 ErrNetworkError 走"退避重试",对 ErrInvalidURL/ErrFileNotFound 直接跳过——不要对所有错误一刀切重试,否则输入类错误会浪费配额。
  • 指标上报:按哨兵分类打点(hash_error_total{sentinel="ErrHTTPError",code="403"}),便于发现出口 IP 健康度问题。

常见误用

  • 字符串匹配 err.Error()strings.Contains(err.Error(), "403") 跨版本会脆裂。HTTPStatusCode(err) 是稳定 API。
  • 重试 ErrInvalidBase64:输入数据不会因为你多试几次就变成合法 base64,重试只是浪费。
  • context.DeadlineExceeded 不是 SDK 哨兵:它来自 ctxerrors.Is(err, context.DeadlineExceeded)true 时表示是你自己的超时,不是 SDK 网络错误,不要混入 ErrNetworkError 分支。

错误处理决策流程(细化)

下图在原决策流程基础上,按哨兵错误细化分支与重试策略。

flowchart TD CALL["调用 Hash / Identify 系列"] --> CKErr{"返回 err?"} CKErr -->|"nil"| SUCC["正常路径
处理 value results"] CKErr -->|"非 nil"| IS

IS -->|"ErrInvalidURL"| BADURL["记录非法 URL<br/>跳过不重试"]
IS -->|"ErrInvalidBase64"| BADB64["校验输入<br/>不重试"]
IS -->|"ErrFileNotFound"| BADFILE["os.Stat 预检<br/>不重试"]
IS -->|"ErrNoFaviconFound"| NOICON["记为无图标<br/>不重试"]
IS -->|"ErrHTTPError"| HTTP{"HTTP 状态码?"}
IS -->|"ErrNetworkError"| NET["指数退避重试<br/>检查代理 DNS"]
IS -->|"ErrReaderFailed"| RDR{"Reader 可重放?"}
IS -->|"其他"| UNKNOWN["记录上下文<br/>上报未知错误"]

HTTP -->|"403 或 429"| BLOCKED["换出口代理<br/>退避后重试"]
HTTP -->|"5xx"| RETRY5["服务端错误<br/>退避重试上限 3 次"]
HTTP -->|"4xx 其他"| SKIP4["客户端错误<br/>跳过不重试"]

RDR -->|"是"| RDRRETRY["重新打开 Reader 重试"]
RDR -->|"否"| RDRSKIP["跳过记上游故障"]

SUCC --> END["继续"]
BADURL --> END
BADB64 --> END
BADFILE --> END
NOICON --> END
BLOCKED --> END
RETRY5 --> END
SKIP4 --> END
NET --> END
RDRRETRY --> END
RDRSKIP --> END
UNKNOWN --> END

style SUCC fill:#48bb78,color:#fff
style BLOCKED fill:#ed8936,color:#fff
style RETRY5 fill:#ed8936,color:#fff
style NET fill:#ed8936,color:#fff
style BADURL fill:#f56565,color:#fff
style BADB64 fill:#f56565,color:#fff
style BADFILE fill:#f56565,color:#fff
style NOICON fill:#f56565,color:#fff
style SKIP4 fill:#f56565,color:#fff
style RDRSKIP fill:#f56565,color:#fff```

下一篇:构建与发布

基于 MIT 许可证发布