Go SDK
v1.x 稳定 Go 1.21+ API 冻结将 IconHash 的哈希与指纹能力直接嵌入你的 Go 程序,无需启动外部服务。
为什么要用 SDK 而不是 CLI?
当你需要在已有 Go 服务里按 URL 实时识别组件、做资产测绘、或对一批图标做批量去重时,反复拉起 CLI 进程的代价过高。SDK 把 pkg/hasher 与 pkg/fingerprint 直接暴露给你:一次 import,零额外进程,还能复用 context.Context 做超时与取消。
包结构
import 路径
import (
"github.com/cyberspacesec/iconhash-skills/pkg/hasher"
"github.com/cyberspacesec/iconhash-skills/pkg/fingerprint"
)该用哪个函数?
决策口诀
- 只要哈希值 →
Hash*系列,返回string,轻量、零依赖。 - 要识别出是哪个组件/服务 →
Identify*系列,必须传*fingerprint.DB。 - 想一次拿全(发现图标→哈希→匹配)→ 直接
IdentifySite,等价于 CLI 的identify子命令。
核心 API 速查
速查表(可折叠)
| 包 | 函数 / 方法 | 入参 | 返回 | 备注 |
|---|---|---|---|---|
hasher | New(opts) | *HashOptions | *IconHasher | 构造器,opts==nil 用默认 |
hasher | DefaultOptions() | — | *HashOptions | 超时 10s、UA 伪 Chrome |
hasher | NewOptionsWithProxy(url, timeout, insecure) | string, time.Duration, bool | *HashOptions, error | 一键配代理 |
*IconHasher | HashFromURL | ctx, url | (string, error) | 网络来源 |
*IconHasher | HashFromFile | path | (string, error) | 本地文件 |
*IconHasher | HashFromBase64 | b64 | (string, error) | base64 字符串 |
*IconHasher | HashFromBytes | []byte | (string, error) | 内存字节 |
*IconHasher | HashFromReader | io.Reader | (string, error) | 流式 |
*IconHasher | DiscoverFavicon | ctx, siteURL, opts | ([]string, error) | 仅发现 URL |
*IconHasher | DiscoverAndHash | ctx, siteURL, opts | []DiscoverResult | 发现+哈希 |
*IconHasher | Identify | ctx, siteURL, db, opts | []IdentifyResult | 端到端 |
fingerprint | DefaultDB() | — | *DB | 内置/外部自动选 |
fingerprint | NewDB(entries) | []FingerprintEntry | *DB | 自定义构造 |
fingerprint | NewDBFromJSON(path) | string | (*DB, error) | 文件加载 |
fingerprint | NewDBFromData(data) | []byte | (*DB, error) | 字节加载 |
*DB | Lookup | string | []FingerprintEntry | 精确匹配,未命中返回 nil slice |
*DB | Search | query | []FingerprintEntry | 模糊搜索 |
*DB | AddEntry / RemoveEntry | FingerprintEntry | error / int | 运行时增删(写锁) |
核心 API 签名与语义
稳定 v1.x 冻结以下签名取自源码 pkg/hasher/hasher.go 与 pkg/fingerprint/fingerprint.go,方法均挂在 *IconHasher / *DB 上,包级不存在同名函数——文档前述的 hasher.HashURL(...) 等是简写,实际写法是 h := hasher.New(nil); h.HashFromURL(...)。
哈希系列
// 所有哈希方法均为 *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)参数说明
| 参数 | 类型 | 含义 | 必填 |
|---|---|---|---|
ctx | context.Context | 超时/取消传播,仅 *FromURL 系列强相关 | 是(URL 系) |
url / filePath / base64Data | string | 数据来源定位符 | 是 |
data | []byte | 已解码的图标字节 | 是 |
r | io.Reader | 任意流(stdin、管道、网络流) | 是 |
useUint32 | bool | 单次返回 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。
常见误用
- 忘检查
error:h, _ := h.HashFromURL(ctx, url); db.Lookup(h)——网络失败时h == "",Lookup("")永远返回空,静默漏报。务必if err != nil { ... }。 ctx透传缺失:HashFromURL(context.Background(), ...)在批量场景会失去上层超时控制。应传业务ctx,让父超时能级联取消。- 复用
*IconHasher跨 goroutine 改Options:Options()返回的是副本,改副本不影响实例;但要改实例配置应重新New,不要在并发中修改。 - 把
HashFromBase64当 URL 用:HashFromBase64("https://...")不会下载,会把整段 URL 当 base64 解码并报ErrInvalidBase64。
发现与识别系列
// 发现:返回站点上所有 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)结果类型
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——它们是"尽力而为"的批量接口,逐条结果里的Err(DiscoverResult)或空Fingerprint(IdentifyResult)承载失败信息。这是为了避免单个图标失败导致整批丢失。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。 opts传nil:DiscoverFavicon接受nil(用默认),但生产建议显式传DefaultDiscoverOptions()并按需调整,避免版本升级后默认行为变化。db传nil:Identify系列要求db != nil,传nil会 panic。用fingerprint.DefaultDB()兜底。
指纹库系列
// 构造
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) errortype 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 参数说明
| 参数 | 类型 | 含义 |
|---|---|---|
hash | string | HashFrom* 返回的字符串哈希;与 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 { ... }。- 并发中调
AddEntry:AddEntry取写锁,与并发Lookup(读锁)互斥;高频查询场景里夹一次写会阻塞所有读。冷启动期写完再并发读,运行期避免写。 Hash字段当int32用:FingerprintEntry.Hash是string,Lookup入参也是string。从HashFrom*拿到的string可直接传入,无需类型转换。NewDBFromJSON不检查文件存在:路径错误会返回error,不要_忽略,否则拿到空 DB 后所有Lookup都返回空。
类型关系图
下图展示 HashOptions / IconHasher / DB / FingerprintEntry / IdentifyResult 等核心类型之间的构造与依赖关系(实线=构造/持有,虚线=使用/返回)。
类型记忆法
HashOptions是配置(值语义,可拷贝)→ 喂给New造IconHasher。IconHasher是无状态服务(内部只读options+httpClient)→ 可安全跨 goroutine 复用。DB是带读写锁的索引→ 只读查询并发安全,写操作需独占。*Result/FingerprintEntry是值类型→ 拿到即拷贝,可放心持有。
哈希计算
基础用法
稳定 入门文档为可读性用
hasher.HashURL(...)等简写,真实方法名是HashFromURL等挂在*IconHasher上。下方"完整可运行示例"使用真实签名。
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)
}完整可运行示例(含错误处理)
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:
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 参数说明
| 参数 | 类型 | 含义 | 推荐值 |
|---|---|---|---|
proxyURL | string | 支持 http:// / https:// / socks5:// | 公司出口代理 |
timeout | time.Duration | HTTP client 超时 | 公网 5–10s,内网 3s |
insecureSkipVerify | bool | 是否跳过 TLS 校验 | 公网 false,内网可 true |
返回值结构
- 成功返回
*HashOptions,其HTTPClient已绑定http.ProxyURL(proxy)的Transport。 - 失败返回
nil, error:仅当proxyURL无法url.Parse时(如协议不合法)。网络层错误在实际请求时才暴露,不会在此提前报错。
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.FSFull vs Lite 构建
- Full 构建:指纹 JSON 通过
go:embed编译进二进制,IsEmbedded() == true,单文件即用,体积约 +1MB。 - Lite 构建:不内嵌指纹,首次调用时从本地缓存或远程仓库拉取,适合对二进制体积敏感的场景。
- 调用
DefaultDB()时无需关心模式,库内部会自动选路径——见下方流程图。
加载默认库
推荐 零配置db := fingerprint.DefaultDB()使用场景
- 绝大多数情况直接用
DefaultDB():Full 构建读embed.FS零 IO,Lite 构建自动落缓存。它还会合并环境变量ICONHASH_FINGERPRINT_DB指向的库,便于不改代码切换数据源。 - 仅当你有明确的私有库且不想让内置库污染结果时,才用
NewDBFromJSON替代。
查询
稳定db := fingerprint.DefaultDB()
result := db.Lookup(-1161367058)
if result != nil {
fmt.Println(result.Service) // nginx
}上例为旧式简写,真实签名是 Lookup(hash string) []FingerprintEntry
源码里 Lookup 入参为 string、返回 []FingerprintEntry(slice,非单指针)。正确写法:
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返回nilslice 时len==0,用range安全,但[0]会 panic。 - 把
Search当Lookup:Search是子串扫描、O(n);Lookup是哈希表、O(1)。批量场景误用Search会显著拖慢。
从自定义文件加载
进阶// 从 JSON 文件
db, _ := fingerprint.NewDBFromJSON("my-fingerprints.json")
// 从 JSON 字节
db, _ := fingerprint.NewDBFromData(jsonBytes)使用场景
NewDBFromJSON(path):私有指纹库已落盘成 JSON,一次性加载为*DB。完全替代内置库,结果里不会混入内置指纹。NewDBFromData(data):JSON 来自网络/API 响应/数据库 blob,未落盘。无 IO,适合动态构建。
常见误用
- 忽略
error:db, _ := NewDBFromJSON(...)在文件不存在或 JSON 非法时返回nil,后续db.Lookup直接 nil panic。务必检查err。 - JSON 格式错:必须是数组(
[{...}, {...}]),不是单对象、不是包了一层{"fingerprints": [...]}。FingerprintEntry.Hash是字符串,写成数字会被解码失败。
合并内置与自定义
进阶 多源融合自定义指纹 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"
}
]字段说明:
hash:int32,必填;与db.Lookup入参类型一致。service:组件/服务名,建议小写连字符。category:自由分类,如web-server、cms、router。tags、description:可空,用于在 CLI/SDK 输出时补充上下文。
一键识别(推荐)
IdentifySite 是端到端封装,等价于 CLI 的 identify:
为什么推荐它?
IdentifySite 一次调用即完成 发现图标 → 下载 → 哈希 → 查库 → 返回结构化结果,内部已处理好"一个站点多个图标"的情况(返回 []IdentifyResult)。绝大多数资产测绘场景用它就够了,省去手动拼装 DiscoverFavicons + HashURL + Lookup 的样板代码。
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 的方法,签名:
func (h *IconHasher) Identify(ctx context.Context, siteURL string, db *fingerprint.DB, opts *DiscoverFaviconOptions) []IdentifyResult正确写法:
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 哈希是否偏离已知基线(识别被篡改/仿冒)。
- 不要用它做"单纯哈希":只要哈希走
DiscoverAndHash或HashFromURL,Identify多一次Lookup,开销不必要。
识别函数矩阵
并发批量
并发安全 需自备信号量SDK 本身是并发安全的,可直接用 goroutine 批量:
并发批量三个坑
- 不要无界 goroutine:URL 列表大时务必用信号量(如示例的
chan struct{})限制并发度,否则会瞬时打满 fd 与内存。 - 共享
*fingerprint.DB无需加锁:内部是只读 map,多 goroutine 并发Lookup安全;但不要在并发过程中调用任何会改写 DB 的方法。 - 结果收集要
sync.Mutex:示例里用mu保护resultsslice 的 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,相当于在并发读中插入串行点,吞吐会被这个写锁"割喉"。移到批处理之外。
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* 返回 string、Lookup 入参为 int32 的设计将在 v1.x 全程保持。v2 计划统一为 Hash 单一返回类型并支持 Result 链式调用,届时会提供迁移脚本。如果你在产线大量调用 SDK,建议封装一层薄适配层,避免将来被动改签名。
错误处理速查
稳定 errors.IsHash* 与 Identify* 都遵循 Go 惯例的 (value, error) 返回。下表是常见错误的处置:
| 错误场景 | 触发函数 | 典型 err | 建议 |
|---|---|---|---|
| 网络超时 | HashURL / IdentifySite | context.DeadlineExceeded | 调小并发度、加代理、设短超时 |
| 非 2xx 响应 | HashURL | unexpected status: 403 | 多为 IP 被封,换出口或重试 |
| 文件不存在 | HashFile | open ...: no such file | 调用前 os.Stat 预检 |
| base64 非法 | HashBase64 | illegal base64 data | 校验输入格式 |
| 指纹未命中 | Lookup | (无 err,返回 nil) | 当作"未知",写入自有库 |
哨兵错误与 errors.Is
稳定 v1.x 引入Hash* 系列返回结构化错误,可用 errors.Is 精确分支处理,不必字符串匹配:
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哨兵错误分类与处置
| 哨兵 | 触发函数 | 含义 | 推荐处置 |
|---|---|---|---|
ErrInvalidURL | HashFromURL | URL 无法 url.Parse | 校验输入,不入重试 |
ErrHTTPError | HashFromURL | 非 2xx 响应 | HTTPStatusCode(err) 取码;403/429 换出口,5xx 可重试 |
ErrNetworkError | HashFromURL | 连接/DNS/TLS/超时 | 指数退避重试,检查代理 |
ErrInvalidBase64 | HashFromBase64 | base64 解码失败 | 校验输入,不重试 |
ErrFileNotFound | HashFromFile | 文件不存在/不可读 | os.Stat 预检,不重试 |
ErrReaderFailed | HashFromReader | io.Reader.Read 失败 | 检查上游流,重试需可重放 |
ErrNoFaviconFound | DiscoverFavicon | 站点无 favicon | 当作"无图标",不重试 |
errors.Is 用法示例
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 哨兵:它来自ctx,errors.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```
下一篇:构建与发布。