热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
智能识别用户提供的代码片段和业务目标,自动补全生成完整、可运行的代码方案,支持多语言、多场景开发任务,提升开发效率并减少调试时间,适用于快速原型和功能实现。
下面给出一个可直接运行的完整示例,包括主服务代码、启动脚本(内置 uvicorn 运行入口)、结构化日志与中间件、异常与输入校验、requirements、Dockerfile、示例请求和单元测试。
文件:main.py 说明:FastAPI 文本分析服务,提供 /health 与 /analyze,包含结构化日志 JSONFormatter、中间件请求日志、异常处理与输入校验,并附带 uvicorn 运行入口。
# main.py
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel, Field
import logging
import json
import os
import re
import time
import uuid
from datetime import datetime, timezone
# -------------------------
# 基本配置与常量
# -------------------------
APP_NAME = "text-analyze-service"
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
MAX_TEXT_LEN = int(os.getenv("MAX_TEXT_LEN", "100000")) # 业务逻辑限制:最大文本长度(去除首尾空白后)
START_TIME = time.time()
# -------------------------
# 结构化日志配置(JSON)
# -------------------------
class JSONFormatter(logging.Formatter):
"""简单的 JSON 日志格式化器,以结构化形式输出日志,便于收集与检索。"""
def format(self, record: logging.LogRecord) -> str:
base = {
"time": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
}
# 将 msg 作为事件名称
base["event"] = record.getMessage()
# 选取我们关心的扩展字段
for key in [
"request_id", "path", "method", "status_code", "duration_ms",
"chars", "lines", "unique_words", "detail"
]:
if hasattr(record, key):
base[key] = getattr(record, key)
# 如果有异常,记录堆栈信息
if record.exc_info:
base["exc_info"] = self.formatException(record.exc_info)
return json.dumps(base, ensure_ascii=False)
logger = logging.getLogger(APP_NAME)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.handlers = [handler]
logger.propagate = False
# -------------------------
# FastAPI 应用与中间件
# -------------------------
app = FastAPI(title=APP_NAME, version=APP_VERSION)
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
"""请求级中间件:生成/透传 request_id,记录耗时与响应码。"""
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
start = time.perf_counter()
try:
response = await call_next(request)
duration_ms = round((time.perf_counter() - start) * 1000, 2)
response.headers["X-Request-ID"] = request_id
logger.info(
"request_completed",
extra={
"request_id": request_id,
"path": request.url.path,
"method": request.method,
"status_code": response.status_code,
"duration_ms": duration_ms,
},
)
return response
except Exception:
duration_ms = round((time.perf_counter() - start) * 1000, 2)
logger.exception(
"request_failed",
extra={
"request_id": request_id,
"path": request.url.path,
"method": request.method,
"duration_ms": duration_ms,
},
)
# 继续抛出异常,由统一异常处理器接管
raise
# -------------------------
# 数据模型
# -------------------------
class AnalyzeReq(BaseModel):
# 不在 Pydantic 中直接限制长度,以便业务逻辑能做更细的处理(trim 后判断长度/空文本)。
text: str = Field(..., description="待分析的文本")
class AnalyzeResp(BaseModel):
chars: int
lines: int
unique_words: int
# -------------------------
# 异常处理
# -------------------------
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""统一 HTTP 异常返回结构。"""
request_id = request.headers.get("X-Request-ID")
logger.warning(
"http_error",
extra={
"request_id": request_id,
"path": request.url.path,
"status_code": exc.status_code,
"detail": exc.detail,
},
)
return JSONResponse(
status_code=exc.status_code,
content={"error": {"code": exc.status_code, "detail": exc.detail}},
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""请求体验证异常(例如字段缺失或类型错误)。"""
request_id = request.headers.get("X-Request-ID")
logger.warning(
"validation_error",
extra={
"request_id": request_id,
"path": request.url.path,
"status_code": 422,
"detail": exc.errors(),
},
)
return JSONResponse(
status_code=422,
content={"error": {"code": 422, "detail": exc.errors()}},
)
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
"""兜底异常处理,避免未捕获错误泄露。"""
request_id = request.headers.get("X-Request-ID")
logger.exception(
"unhandled_error",
extra={"request_id": request_id, "path": request.url.path, "detail": str(exc)},
)
return JSONResponse(
status_code=500,
content={"error": {"code": 500, "detail": "Internal server error"}},
)
# -------------------------
# 路由
# -------------------------
@app.get("/health")
def health():
"""健康检查接口,返回服务状态与版本信息。"""
uptime = round(time.time() - START_TIME, 2)
return {
"status": "ok",
"version": APP_VERSION,
"name": APP_NAME,
"uptime_seconds": uptime,
}
_word_pattern = re.compile(r"\b\w+\b", flags=re.UNICODE)
@app.post("/analyze", response_model=AnalyzeResp)
def analyze(req: AnalyzeReq, request: Request):
"""
文本分析:
- 去除首尾空白后进行统计
- 统计字符数(包含空格与换行符;由于 strip,首尾空白不计入)
- 统计行数(按 splitlines 计算)
- 使用 Unicode 单词边界提取词,大小写不敏感,统计唯一词数
- 空文本处理:返回 400
- 超长文本处理:返回 413
"""
request_id = request.headers.get("X-Request-ID")
# 去除首尾空白
text = (req.text or "").strip()
# 空文本处理
if not text:
raise HTTPException(status_code=400, detail="Text is empty after trimming")
# 超长文本处理(业务逻辑限制)
if len(text) > MAX_TEXT_LEN:
raise HTTPException(
status_code=413,
detail=f"Text is too long (>{MAX_TEXT_LEN} chars after trimming)",
)
# 字符数(采用去除首尾空白后的长度)
chars = len(text)
# 行数:splitlines 会正确处理不同平台换行符
lines = len(text.splitlines()) if text else 0
# 唯一词数:按 Unicode 单词边界匹配,忽略大小写
words = _word_pattern.findall(text.lower())
unique_words = len(set(words))
logger.info(
"analyze_success",
extra={
"request_id": request_id,
"chars": chars,
"lines": lines,
"unique_words": unique_words,
},
)
return AnalyzeResp(chars=chars, lines=lines, unique_words=unique_words)
# -------------------------
# 运行入口(启动脚本)
# -------------------------
if __name__ == "__main__":
# 允许通过环境变量配置端口与是否热重载(开发模式)
import uvicorn
port = int(os.getenv("PORT", "8000"))
reload = os.getenv("RELOAD", "false").lower() in ("1", "true", "yes")
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=reload)
文件:requirements.txt
fastapi>=0.110
uvicorn[standard]>=0.23
pydantic>=2.5
pytest>=7.0
httpx>=0.24
文件:Dockerfile
# Dockerfile
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制代码
COPY . .
EXPOSE 8000
# 默认使用 uvicorn 启动服务
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
文件:README.md
# 文本分析服务
基于 FastAPI 的简单文本分析服务,提供:
- POST /analyze:返回字符数、行数、唯一词数
- GET /health:健康检查
- 结构化日志(JSON 格式)、请求中间件(记录耗时与 request_id)
- 输入校验:空文本返回 400;超长文本(默认 >100000)返回 413
- 统一异常处理
## 本地运行
```bash
# 安装依赖
pip install -r requirements.txt
# 启动服务(默认 8000 端口)
python main.py
# 可选环境变量
# APP_VERSION=1.0.0 MAX_TEXT_LEN=200000 PORT=8000 RELOAD=true
docker build -t text-analyze-service .
docker run -p 8000:8000 -e APP_VERSION=1.0.0 text-analyze-service
curl -s http://localhost:8000/health
curl -s -X POST http://localhost:8000/analyze \
-H 'Content-Type: application/json' \
-H 'X-Request-ID: demo-123' \
-d '{"text": "Hello world\nhello"}'
# 响应示例:{"chars":17,"lines":2,"unique_words":2}
import requests
resp = requests.post("http://localhost:8000/analyze", json={"text": "你好 世界\n世界"})
print(resp.json()) # {'chars': 8, 'lines': 2, 'unique_words': 2}
{"time":"2024-01-01T00:00:00+00:00","level":"INFO","logger":"text-analyze-service","event":"request_completed","request_id":"demo-123","path":"/analyze","method":"POST","status_code":200,"duration_ms":1.23}
{"time":"2024-01-01T00:00:00+00:00","level":"INFO","logger":"text-analyze-service","event":"analyze_success","request_id":"demo-123","chars":17,"lines":2,"unique_words":2}
运行:
pytest -q
文件:tests/test_app.py
```python
# tests/test_app.py
import pytest
from fastapi.testclient import TestClient
import main
client = TestClient(main.app)
def test_health():
r = client.get("/health")
assert r.status_code == 200
data = r.json()
assert data["status"] == "ok"
assert "version" in data
def test_analyze_basic():
payload = {"text": "Hello world\nhello"}
r = client.post("/analyze", json=payload, headers={"X-Request-ID": "test-req-1"})
assert r.status_code == 200
data = r.json()
assert data["chars"] == 17 # "Hello world"=11 + "\n"=1 + "hello"=5
assert data["lines"] == 2
assert data["unique_words"] == 2 # hello, world
def test_analyze_chinese():
payload = {"text": "你好 世界\n世界"}
r = client.post("/analyze", json=payload)
assert r.status_code == 200
data = r.json()
assert data["chars"] == 8 # 2 + 1 + 2 + 1 + 2
assert data["lines"] == 2
assert data["unique_words"] == 2 # 你好, 世界
def test_analyze_empty_after_trim():
payload = {"text": " \n "}
r = client.post("/analyze", json=payload)
assert r.status_code == 400
data = r.json()
assert data["error"]["detail"] == "Text is empty after trimming"
def test_analyze_too_long():
long_text = "a" * (main.MAX_TEXT_LEN + 1)
r = client.post("/analyze", json={"text": long_text})
assert r.status_code == 413
data = r.json()
assert "too long" in data["error"]["detail"]
def test_bad_payload_validation():
# 缺少 text 字段 -> 422
r = client.post("/analyze", json={})
assert r.status_code == 422
data = r.json()
assert data["error"]["code"] == 422
说明与设计要点:
下面给出一个完整的可复用实现,包含:
代码包含必要的注释,方便后续维护与扩展。
// RegisterForm.jsx import React, { useRef, useState } from 'react';
/**
// 计算当前值的所有错误 function computeErrors(vs) { const nextErrors = {}; for (const name in validators) { const fn = validators[name]; if (typeof fn === 'function') { const msg = fn(vs[name], vs); if (msg) nextErrors[name] = msg; } } return nextErrors; }
const isThrottled = () => Date.now() - lastSubmitTimeRef.current < throttleMs;
const isValid = Object.keys(computeErrors(values)).length === 0; const canSubmit = isValid && !loading && !isThrottled();
function handleChange(e) { const { name, value } = e.target; setValues(prev => { const vs = { ...prev, [name]: value }; if (validateOnChange) { setErrors(computeErrors(vs)); } return vs; }); }
function handleBlur(e) { const { name } = e.target; setTouched(prev => ({ ...prev, [name]: true })); if (validateOnBlur) { setErrors(prev => { const next = computeErrors(values); return next; }); } }
function resetForm() { setValues(initialValues || {}); setErrors({}); setTouched({}); setSubmitError(''); setSubmitSuccess(''); }
async function handleSubmit(e) { e.preventDefault(); setSubmitError(''); setSubmitSuccess('');
// 节流防重复提交
if (loading || isThrottled()) {
setSubmitError('提交过于频繁,请稍后再试');
return;
}
const nextErrors = computeErrors(values);
setErrors(nextErrors);
if (Object.keys(nextErrors).length > 0) {
setSubmitError('请修正表单错误后再提交');
return;
}
lastSubmitTimeRef.current = Date.now();
setLoading(true);
try {
await onSubmit(values, { resetForm, setSubmitError, setSubmitSuccess });
} catch (err) {
setSubmitError(err?.message || '提交失败,请稍后重试');
} finally {
setLoading(false);
}
}
return { values, errors, touched, loading, submitError, submitSuccess, canSubmit, isValid, isThrottled: isThrottled(), handleChange, handleBlur, handleSubmit, resetForm, setSubmitError, setSubmitSuccess, setValues, }; }
// 简单样式(可替换为 CSS/SCSS 或 Tailwind) const styles = { form: { maxWidth: 400, margin: '24px auto', padding: 16, border: '1px solid #e5e7eb', borderRadius: 8, fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial', background: '#fff', }, label: { display: 'block', fontSize: 14, marginBottom: 6, color: '#374151' }, input: { width: '100%', padding: '8px 10px', marginBottom: 8, border: '1px solid #d1d5db', borderRadius: 6, fontSize: 14, outline: 'none', }, inputInvalid: { borderColor: '#ef4444', background: '#fff7f7' }, help: { fontSize: 12, color: '#6b7280', marginBottom: 8 }, errorText: { fontSize: 12, color: '#ef4444', marginBottom: 8 }, successText: { fontSize: 13, color: '#10b981', marginBottom: 8 }, button: { width: '100%', padding: '10px 12px', fontSize: 15, border: 'none', borderRadius: 6, background: '#3b82f6', color: '#fff', cursor: 'pointer', }, buttonDisabled: { background: '#93c5fd', cursor: 'not-allowed' }, };
/**
export default function RegisterForm() { const { values, errors, touched, loading, submitError, submitSuccess, canSubmit, isValid, handleChange, handleBlur, handleSubmit, resetForm, setSubmitError, setSubmitSuccess, } = useForm({ initialValues: { username: '', email: '', password: '' }, validators, throttleMs: 1500, onSubmit: async (formValues, helpers) => { try { const res = await fetch('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formValues), });
// 解析响应
let data = null;
try {
data = await res.json();
} catch {
// 响应非 JSON 时忽略
}
if (!res.ok) {
const msg =
data?.error ||
data?.message ||
`注册失败(${res.status})`;
helpers.setSubmitError(msg);
return;
}
// 成功:重置表单并给出提示
helpers.setSubmitSuccess('注册成功!');
helpers.resetForm();
} catch (err) {
helpers.setSubmitError(err?.message || '网络错误,请稍后重试');
}
},
});
// 提供一个清理提示的按钮(可选) function clearMessages() { setSubmitError(''); setSubmitSuccess(''); }
return (
go.mod module github.com/example/concurrent-fetch
go 1.21
main.go package main
import ( "context" "encoding/json" "flag" "fmt" "os" "os/signal" "path/filepath" "sort" "strings" "syscall" "time" )
type URLResult struct {
URL string json:"url"
Bytes int json:"bytes"
Headings int json:"headings"
Attempts int json:"attempts"
Cached bool json:"cached"
DurationMS int64 json:"duration_ms"
Error string json:"error,omitempty"
}
type Totals struct {
URLs int json:"urls"
Successes int json:"successes"
Failures int json:"failures"
Bytes int json:"bytes"
Headings int json:"headings"
}
type Meta struct {
Workers int json:"workers"
Timeout string json:"timeout"
Retries int json:"retries"
Backoff string json:"backoff"
CacheDir string json:"cache_dir"
CacheTTL string json:"cache_ttl"
Started string json:"started"
Duration string json:"duration"
}
type Summary struct {
Results []URLResult json:"results"
Totals Totals json:"totals"
Meta Meta json:"meta"
}
// Example usage: // go run ./cmd -workers=8 -timeout=4s -retries=2 -backoff=300ms https://example.com https://golang.org // go run . -urls-file=urls.txt func main() { workers := flag.Int("workers", 4, "number of concurrent workers") timeout := flag.Duration("timeout", 5time.Second, "per-request timeout") retries := flag.Int("retries", 2, "retries on transient errors") backoff := flag.Duration("backoff", 300time.Millisecond, "base backoff between retries") cacheDir := flag.String("cache-dir", "", "cache directory (default: $TMP/concurrent-fetch-cache)") cacheTTL := flag.Duration("cache-ttl", 10*time.Minute, "use cached content if not older than this duration") urlsFile := flag.String("urls-file", "", "path to file with URLs (one per line); if empty, takes URLs from args") logLevel := flag.String("log-level", "info", "log level: debug, info, warn, error") flag.Parse()
urls := []string{}
if *urlsFile != "" {
b, err := os.ReadFile(*urlsFile)
if err != nil {
fmt.Fprintf(os.Stderr, "error reading urls file: %v\n", err)
os.Exit(1)
}
for _, l := range strings.Split(string(b), "\n") {
l = strings.TrimSpace(l)
if l != "" && !strings.HasPrefix(l, "#") {
urls = append(urls, l)
}
}
} else if flag.NArg() > 0 {
urls = flag.Args()
} else {
urls = []string{
"https://example.com",
"https://example.com/",
}
}
if len(urls) == 0 {
fmt.Fprintln(os.Stderr, "no URLs provided")
os.Exit(1)
}
if *cacheDir == "" {
*cacheDir = filepath.Join(os.TempDir(), "concurrent-fetch-cache")
}
logger := NewLogger(os.Stderr, ParseLevel(*logLevel))
logger.Info("starting", Fields{
"workers": *workers,
"timeout": timeout.String(),
"retries": *retries,
"backoff": backoff.String(),
"cache_dir": *cacheDir,
"cache_ttl": cacheTTL.String(),
"urls": len(urls),
})
if err := os.MkdirAll(*cacheDir, 0o755); err != nil {
logger.Error("failed to create cache dir", Fields{"err": err.Error(), "dir": *cacheDir})
os.Exit(1)
}
cache := NewFileCache(*cacheDir)
fetcher := &Fetcher{
Timeout: *timeout,
Retries: *retries,
Backoff: *backoff,
Cache: cache,
TTL: *cacheTTL,
Logger: logger,
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 2)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
go func() {
s := <-sigCh
logger.Warn("signal received, shutting down", Fields{"signal": s.String()})
cancel()
// Second signal forces exit
s = <-sigCh
logger.Error("second signal received, forcing exit", Fields{"signal": s.String()})
os.Exit(2)
}()
start := time.Now()
jobs := make(chan Job)
resultsCh := make(chan URLResult)
// Start workers
go func() {
RunWorkers(ctx, *workers, jobs, func(ctx context.Context, j Job) URLResult {
t0 := time.Now()
data, attempts, cached, err := fetcher.Fetch(ctx, j.URL)
res := URLResult{
URL: j.URL,
Attempts: attempts,
Cached: cached,
DurationMS: time.Since(t0).Milliseconds(),
}
if err != nil {
res.Error = err.Error()
return res
}
res.Bytes = len(data)
res.Headings = CountHeadings(data)
return res
}, resultsCh)
}()
// Feed jobs
go func() {
defer close(jobs)
for _, u := range urls {
select {
case <-ctx.Done():
return
case jobs <- Job{URL: strings.TrimSpace(u)}:
}
}
}()
// Collect results
collected := make([]URLResult, 0, len(urls))
for r := range resultsCh {
collected = append(collected, r)
if len(collected) == len(urls) {
break
}
}
// Ensure workers stop
cancel()
// Aggregate
sort.Slice(collected, func(i, j int) bool { return collected[i].URL < collected[j].URL })
var totals Totals
totals.URLs = len(collected)
for _, r := range collected {
if r.Error == "" {
totals.Successes++
totals.Bytes += r.Bytes
totals.Headings += r.Headings
} else {
totals.Failures++
}
}
sum := Summary{
Results: collected,
Totals: totals,
Meta: Meta{
Workers: *workers,
Timeout: timeout.String(),
Retries: *retries,
Backoff: backoff.String(),
CacheDir: *cacheDir,
CacheTTL: cacheTTL.String(),
Started: start.Format(time.RFC3339Nano),
Duration: time.Since(start).String(),
},
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(sum); err != nil {
logger.Error("failed to write summary", Fields{"err": err.Error()})
os.Exit(1)
}
logger.Info("done", Fields{"duration": time.Since(start).String(), "successes": totals.Successes, "failures": totals.Failures})
}
logger.go package main
import ( "encoding/json" "io" "strings" "sync" "time" )
type Level int
const ( LevelDebug Level = iota LevelInfo LevelWarn LevelError )
func ParseLevel(s string) Level { switch strings.ToLower(strings.TrimSpace(s)) { case "debug": return LevelDebug case "warn", "warning": return LevelWarn case "error": return LevelError default: return LevelInfo } }
type Fields map[string]any
type Logger struct { mu sync.Mutex enc *json.Encoder level Level }
func NewLogger(w io.Writer, level Level) *Logger { return &Logger{enc: json.NewEncoder(w), level: level} }
func (l *Logger) log(level Level, msg string, fields Fields) { if level < l.level { return } entry := map[string]any{ "ts": time.Now().Format(time.RFC3339Nano), "lvl": [...]string{"debug", "info", "warn", "error"}[level], "msg": msg, "pid": getPID(), "prog": getProg(), } for k, v := range fields { entry[k] = v } l.mu.Lock() defer l.mu.Unlock() _ = l.enc.Encode(entry) }
func (l *Logger) Debug(msg string, f Fields) { l.log(LevelDebug, msg, f) } func (l *Logger) Info(msg string, f Fields) { l.log(LevelInfo, msg, f) } func (l *Logger) Warn(msg string, f Fields) { l.log(LevelWarn, msg, f) } func (l *Logger) Error(msg string, f Fields) { l.log(LevelError, msg, f) }
func getPID() int { return osGetpid() }
func getProg() string { return osArgs0() }
logger_os.go package main
import "os"
func osGetpid() int { return os.Getpid() } func osArgs0() string { if len(os.Args) > 0 { return os.Args[0] } return "app" }
worker.go package main
import ( "context" "sync" )
type Job struct { URL string }
type Handler func(context.Context, Job) URLResult
func RunWorkers(ctx context.Context, n int, jobs <-chan Job, handler Handler, out chan<- URLResult) { var wg sync.WaitGroup wg.Add(n) for i := 0; i < n; i++ { go func(id int) { defer wg.Done() for { select { case <-ctx.Done(): return case j, ok := <-jobs: if !ok { return } out <- handler(ctx, j) } } }(i) } go func() { wg.Wait() close(out) }() }
fetcher.go package main
import ( "context" "errors" "fmt" "io" "math/rand" "net" "net/http" "time" )
type Cache interface { Get(url string, ttl time.Duration) ([]byte, bool, error) Put(url string, data []byte) error }
type Fetcher struct { Timeout time.Duration Retries int Backoff time.Duration Cache Cache TTL time.Duration Logger *Logger Client *http.Client // optional; if nil, constructed per request }
func (f *Fetcher) httpClient() *http.Client { if f.Client != nil { return f.Client } return &http.Client{ Timeout: f.Timeout, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, MaxIdleConns: 100, MaxConnsPerHost: 20, MaxIdleConnsPerHost: 20, IdleConnTimeout: 90 * time.Second, DialContext: (&net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, }, } }
func (f *Fetcher) Fetch(ctx context.Context, url string) ([]byte, int, bool, error) { if f.Cache != nil && f.TTL > 0 { if data, ok, err := f.Cache.Get(url, f.TTL); err == nil && ok { f.Logger.Debug("cache hit", Fields{"url": url}) return data, 0, true, nil } }
attempts := 0
var lastErr error
for {
select {
case <-ctx.Done():
return nil, attempts, false, ctx.Err()
default:
}
attempts++
data, err := f.fetchOnce(ctx, url)
if err == nil {
if f.Cache != nil {
_ = f.Cache.Put(url, data)
}
return data, attempts, false, nil
}
lastErr = err
if attempts > f.Retries {
break
}
// backoff with jitter
delay := f.Backoff + time.Duration(rand.Int63n(int64(f.Backoff)/2+1))
if delay <= 0 {
delay = 100 * time.Millisecond
}
f.Logger.Warn("fetch failed, retrying", Fields{"url": url, "attempt": attempts, "err": err.Error(), "sleep": delay.String()})
t := time.NewTimer(delay)
select {
case <-ctx.Done():
t.Stop()
return nil, attempts, false, ctx.Err()
case <-t.C:
}
}
return nil, attempts, false, fmt.Errorf("fetch failed after %d attempt(s): %w", attempts, lastErr)
}
func (f *Fetcher) fetchOnce(ctx context.Context, url string) ([]byte, error) { client := f.httpClient() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", "concurrent-fetch/1.0 (+https://example)") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 500 { return nil, fmt.Errorf("server error: %d", resp.StatusCode) } if resp.StatusCode >= 400 { // treat 4xx as non-retryable error return nil, fmt.Errorf("client error: %d", resp.StatusCode) } limited := &io.LimitedReader{R: resp.Body, N: 10 << 20} // 10MB safety limit data, err := io.ReadAll(limited) if err != nil { return nil, err } if limited.N == 0 { return nil, errors.New("response too large") } return data, nil }
parser.go package main
import ( "bytes" "strings"
"golang.org/x/net/html"
)
func CountHeadings(content []byte) int { // Try HTML first if n, ok := countHTMLHeadings(content); ok { return n } // Fallback: Markdown-style headings return countMarkdownHeadings(content) }
func countHTMLHeadings(content []byte) (int, bool) { doc, err := html.Parse(bytes.NewReader(content)) if err != nil { return 0, false } count := 0 var walk func(*html.Node) walk = func(n *html.Node) { if n.Type == html.ElementNode { switch strings.ToLower(n.Data) { case "h1", "h2", "h3", "h4", "h5", "h6": count++ } } for c := n.FirstChild; c != nil; c = c.NextSibling { walk(c) } } walk(doc) return count, true }
func countMarkdownHeadings(content []byte) int { lines := strings.Split(string(content), "\n") count := 0 for _, l := range lines { lt := strings.TrimSpace(l) if lt == "" || strings.HasPrefix(lt, "<") { // probably HTML; skip noisy matches continue } if strings.HasPrefix(lt, "#") { count++ } } return count }
cache.go package main
import ( "crypto/sha256" "encoding/hex" "errors" "os" "path/filepath" "time" )
type FileCache struct { dir string }
func NewFileCache(dir string) *FileCache { return &FileCache{dir: dir} }
func (c *FileCache) key(url string) string { sum := sha256.Sum256([]byte(url)) return filepath.Join(c.dir, hex.EncodeToString(sum[:])+".cache") }
func (c *FileCache) Get(url string, ttl time.Duration) ([]byte, bool, error) { path := c.key(url) st, err := os.Stat(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil, false, nil } return nil, false, err } if ttl > 0 && time.Since(st.ModTime()) > ttl { return nil, false, nil } data, err := os.ReadFile(path) if err != nil { return nil, false, err } return data, true, nil }
func (c *FileCache) Put(url string, data []byte) error { path := c.key(url) tmp := path + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { return err } return os.Rename(tmp, path) }
go.sum // intentionally empty; no external dependencies recorded for stdlib only
parser_test.go package main
import "testing"
func TestCountHeadingsHTML(t *testing.T) { html := `
no
` got := CountHeadings([]byte(html)) if got != 3 { t.Fatalf("expected 3 headings, got %d", got) } }func TestCountHeadingsMarkdown(t *testing.T) { md := `
some text
normal
not a heading # text ` got := CountHeadings([]byte(md)) if got != 3 { t.Fatalf("expected 3 headings, got %d", got) } }
fetcher_test.go package main
import ( "context" "net/http" "net/http/httptest" "os" "path/filepath" "sync/atomic" "testing" "time" )
func TestFetcherRetry(t *testing.T) { var hits int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n := atomic.AddInt32(&hits, 1) if n <= 2 { http.Error(w, "temporary", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain") w.Write([]byte("
logger := NewLogger(os.Stderr, LevelError)
f := &Fetcher{
Timeout: 2 * time.Second,
Retries: 3,
Backoff: 10 * time.Millisecond,
TTL: 0,
Logger: logger,
Client: srv.Client(),
}
ctx := context.Background()
data, attempts, cached, err := f.Fetch(ctx, srv.URL)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cached {
t.Fatalf("unexpected cached result")
}
if attempts < 3 {
t.Fatalf("expected at least 3 attempts, got %d", attempts)
}
if string(data) != "<h1>ok</h1>" {
t.Fatalf("unexpected body: %q", string(data))
}
}
func TestFetcherCache(t *testing.T) { dir := t.TempDir() cache := NewFileCache(dir) logger := NewLogger(os.Stderr, LevelError) f := &Fetcher{ Timeout: 1 * time.Second, Retries: 0, Backoff: 10 * time.Millisecond, TTL: 10 * time.Minute, Cache: cache, Logger: logger, Client: &http.Client{Timeout: 1 * time.Second}, }
url := "https://example.com/cached"
// Seed cache
data := []byte("hello # world")
if err := os.WriteFile(filepath.Join(dir, cache.key(url)), data, 0o644); err != nil {
t.Fatalf("write cache: %v", err)
}
got, attempts, cached, err := f.Fetch(context.Background(), url)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !cached {
t.Fatalf("expected cached")
}
if attempts != 0 {
t.Fatalf("expected 0 attempts when cached, got %d", attempts)
}
if string(got) != "hello # world" {
t.Fatalf("unexpected cached content: %q", string(got))
}
}
integration_test.go package main
import ( "context" "encoding/json" "net/http" "net/http/httptest" "os" "sort" "testing" "time" )
func TestEndToEnd(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/a", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("
urls := []string{srv.URL + "/a", srv.URL + "/b"}
logger := NewLogger(os.Stderr, LevelError)
cache := NewFileCache(t.TempDir())
fetcher := &Fetcher{
Timeout: 1 * time.Second,
Retries: 1,
Backoff: 10 * time.Millisecond,
TTL: 0,
Cache: cache,
Logger: logger,
Client: srv.Client(),
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
jobs := make(chan Job)
out := make(chan URLResult)
go func() {
RunWorkers(ctx, 2, jobs, func(ctx context.Context, j Job) URLResult {
t0 := time.Now()
data, attempts, cached, err := fetcher.Fetch(ctx, j.URL)
r := URLResult{URL: j.URL, Attempts: attempts, Cached: cached, DurationMS: time.Since(t0).Milliseconds()}
if err != nil {
r.Error = err.Error()
return r
}
r.Bytes = len(data)
r.Headings = CountHeadings(data)
return r
}, out)
}()
go func() {
defer close(jobs)
for _, u := range urls {
jobs <- Job{URL: u}
}
}()
var results []URLResult
for r := range out {
results = append(results, r)
if len(results) == len(urls) {
break
}
}
if len(results) != 2 {
t.Fatalf("expected 2 results, got %d", len(results))
}
sort.Slice(results, func(i, j int) bool { return results[i].URL < results[j].URL })
if results[0].Headings != 2 || results[1].Headings != 2 {
b, _ := json.MarshalIndent(results, "", " ")
t.Fatalf("unexpected headings count: %s", string(b))
}
}
go.mod note: This code uses golang.org/x/net/html for robust HTML heading parsing. Add the following requirement if your environment needs an explicit module line:
Example usage:
Behavior:
通过智能补全功能,帮助开发者快速生成完整可运行的代码实现,从而提升开发效率、减少编码错误,并加速目标功能的实现。
通过该提示词快速补全业务逻辑代码,减少重复性工作并提升接口开发效率,专注于核心功能实现。
为新手提供有效的代码示例和指导,帮助快速学习语言特性与实现常见功能,降低学习曲线。
高效生成前后端代码片段,无需频繁查阅文档或资料,提升开发一体化效率。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
免费获取高级提示词-优惠即将到期