Go 后端面试复盘:语言、并发与服务实践 · 29.2 有界并发执行任务至29.8 优雅关闭模板
前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。
29.2 有界并发执行任务
go
func RunAll(ctx context.Context, jobs []Job, limit int) error {
if limit <= 0 {
return fmt.Errorf("invalid limit: %d", limit)
}
sem := make(chan struct{}, limit)
errCh := make(chan error, 1)
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(ctx)
defer cancel()
launchLoop:
for _, job := range jobs {
job := job
select {
case sem <- struct{}{}:
case <-ctx.Done():
break launchLoop
}
wg.Add(1)
go func() {
defer wg.Done()
defer func() { <-sem }()
if err := process(ctx, job); err != nil {
select {
case errCh <- err:
cancel()
default:
}
}
}()
}
wg.Wait()
close(errCh)
if err := <-errCh; err != nil {
return err
}
return ctx.Err()
}说明:上例固定等待所有已启动任务返回;process必须配合context停止,否则wg.Wait仍可能阻塞。取消和可用令牌同时就绪时仍可能再启动任务,不保证取消发生后绝无新任务开始。生产代码按业务选择任务组合同,不能将“发出取消信号”当作已经完成收拢。
29.3 Worker Pool
go
func StartWorkers(
ctx context.Context,
workers int,
jobs <-chan Job,
results chan<- Result,
) {
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
result := handle(job)
select {
case results <- result:
case <-ctx.Done():
return
}
}
}
}()
}
go func() {
wg.Wait()
close(results)
}()
}29.4 带 TTL 的简单缓存
go
type entry[V any] struct {
value V
expiresAt time.Time
}
type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]entry[V]
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{items: make(map[K]entry[V])}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
e, ok := c.items[key]
c.mu.RUnlock()
if !ok || !time.Now().Before(e.expiresAt) {
var zero V
return zero, false
}
return e.value, true
}
func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) {
c.mu.Lock()
c.items[key] = entry[V]{
value: value,
expiresAt: time.Now().Add(ttl),
}
c.mu.Unlock()
}
func (c *Cache[K, V]) Delete(key K) {
c.mu.Lock()
delete(c.items, key)
c.mu.Unlock()
}生产缓存还需考虑容量上限、淘汰算法、批量清理、时钟、singleflight、指标和大对象。
29.5 生产者消费者安全关闭
go
func Produce(ctx context.Context, out chan<- Item, items []Item) error {
defer close(out)
for _, item := range items {
select {
case out <- item:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
func Consume(ctx context.Context, in <-chan Item) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case item, ok := <-in:
if !ok {
return nil
}
if err := handle(item); err != nil {
return err
}
}
}
}29.6 指数退避加抖动
go
func Retry(
ctx context.Context,
maxAttempts int,
baseDelay time.Duration,
fn func(context.Context) error,
) error {
if maxAttempts <= 0 || baseDelay <= 0 || baseDelay > 30*time.Second {
return errors.New("positive attempts and baseDelay in (0,30s] required")
}
maxDelay := baseDelay
var lastErr error
for attempt := 0; attempt < maxAttempts; attempt++ {
if err := ctx.Err(); err != nil { return errors.Join(lastErr, err) }
if err := fn(ctx); err == nil {
return nil
} else {
lastErr = err
if !isRetryable(err) {
return err
}
}
if attempt == maxAttempts-1 {
break
}
jitter, err := rand.Int(rand.Reader, big.NewInt(int64(maxDelay)+1))
if err != nil {
return errors.Join(lastErr, err)
}
timer := time.NewTimer(time.Duration(jitter.Int64()))
select {
case <-ctx.Done():
timer.Stop()
return errors.Join(lastErr, ctx.Err())
case <-timer.C:
}
maxDelay = min(30*time.Second, maxDelay*2)
}
return lastErr
}本片段将退避上限固定30秒,正值输入先校验,再饱和倍增,避免左移溢出及0次尝试误报成功。30秒只是教学合同,不是生产推荐;总时间预算由ctx控制,fn仍需配合取消。isRetryable由业务提供,crypto/rand与math/big须显式导入。
29.7 HTTP Client 安全请求模板
go
func FetchJSON[T any](
ctx context.Context,
client *http.Client,
url string,
maxBody int64,
) (T, error) {
var zero T
if client == nil || maxBody < 0 || maxBody == math.MaxInt64 {
return zero, errors.New("client required and maxBody must allow maxBody+1")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return zero, fmt.Errorf("create request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return zero, fmt.Errorf("send request: %w", err)
}
defer resp.Body.Close()
body := io.LimitReader(resp.Body, maxBody+1)
data, err := io.ReadAll(body)
if err != nil {
return zero, fmt.Errorf("read response: %w", err)
}
if int64(len(data)) > maxBody {
return zero, fmt.Errorf("response body too large")
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return zero, fmt.Errorf("unexpected status %d: %s",
resp.StatusCode, strings.TrimSpace(string(data)))
}
var result T
if err := json.Unmarshal(data, &result); err != nil {
return zero, fmt.Errorf("decode response: %w", err)
}
return result, nil
}真实系统还需做 URL/SSRF 校验、错误体脱敏、Content-Type 校验和可观测性。
29.8 优雅关闭模板
go
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: newHandler(),
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
}
errCh := make(chan error, 1)
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
}()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigCh)
select {
case sig := <-sigCh:
log.Printf("received signal: %v", sig)
case err := <-errCh:
log.Fatalf("server error: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown error: %v", err)
}
}继续阅读
- 排错
- 排错
- 排错