Golang开发工程师求职经验分享

Golang开发工程师求职经验分享 你好,

我是Marco,从小时候在C64上开始,我就热爱编程和软件开发。我目前没有工作 😓。

不久前我爱上了Go语言。有人给了我一个任务,要求我用Go来解决,以展示我的技能。

以下是我对该任务的解决方案。

Gitea: Git with a cup of tea

关于我解决方案的视频在gittea的readme.md文件中。

这是任务内容,如果你喜欢我的解决方案,请雇佣我,或者帮助我变得更好。

组织要求

目标

应用需求

任务 1

任务 2 - 库存导入

任务 3 - 跨国/州交付时间

任务 4 - 库存和交付时间 API

任务 5 - 预订 API

自动释放订单处理目标

组织要求 请使用基于Git的版本控制平台(如GitHub的私有模式)与我们分享您的所有工作,以便我们能看到您的开发进度并测试最终结果。请始终使用此电子邮件地址分享代码。

目标 我们的客户是一家全球在线商店运营商,业务遍布全球众多国家。他们建立了针对德国、奥地利和瑞士客户独特需求定制的国家特定在线商店。此外,他们还运营着庞大的、全国性的商店,专门为所有其他欧盟国家、美国以及世界其他地区的客户服务。

这种全球覆盖由一个广泛的仓库网络支持,这些仓库战略性地分布在多个国家。这些仓库不仅确保产品的高效分发,还为全球各地的客户提供了流畅的购物体验。

对于客户销售的每一种产品,在其每个仓库中都有特定的可用数量。然而,这些产品的可用性差异很大;一些仓库可能持有大量特定产品,而另一些可能数量有限,或者根本没有库存。一个仓库可以为多个在线商店供货。

仓库会生成包含每个产品可用库存信息的CSV文件。目前,每个电子商务应用都必须单独处理所有给定的库存信息。每个仓库生成的库存信息文件会发送给所有商店,并在那里进行处理。每个商店都持有其自己的库存信息副本。这导致了错误库存和交付时间被宣传的问题。例如,德国和瑞士商店都知道我们在欧盟仓库有x件商品,如果两个客户在两个不同的商店购买相同的产品,我们就会超售该商品。

为了避免这种情况,我们希望引入一个中央实例来处理库存信息,并通过API接受商店实例关于当前可用库存的查询。此应用程序应使用Golang编写。

应用需求

  • 应用程序必须使用Golang编写。
  • 如果使用其他服务(数据库、消息代理等),必须提供docker-compose.yml文件。
  • 必须提供包含如何运行此项目说明的README文件。
  • 必须提供关于如何使用API的文档(可以在README、API规范、Postman集合等中)。
  • 用于测试应用程序的导入文件必须在请求中提供。
  • 实施的预计持续时间应为一天。

任务 1 在开始之前,请在GitHub上创建一个免费账户,并在那里创建一个新的代码仓库。稍后您将与我们分享包含源代码的仓库。

任务 2 - 库存导入 我们详细掌握每个仓库中特定产品及其当前库存水平的信息。每个仓库都运行着自己的数据库来保存这些信息。没有包含每个产品和仓库库存的集中式数据库。每个仓库使用的软件无法修改,只允许文件导出。

每个仓库每小时生成一个包含当前库存的CSV文件。此CSV文件与集中存储同步。仅支持完全更新,这意味着每小时生成的每个文件都包含所有产品。估计每个文件的大小为700,000个产品/行。

我们的应用程序必须读取这些CSV文件并将其导入本地数据库。

我们需要开发的应用程序将从本地文件系统读取CSV文件。所有文件将保存在一个目录中。我们不需要关心集中存储。

每个仓库创建的文件名具有以下结构: Time[RFC-3339, ISO_8601]-WarehouseId-stock.csv

示例:

$ ls -a | sort
2023-11-09T15:02:17+00:00-CH-stock.csv
2023-11-09T16:01:14+00:00-CH-stock.csv
2023-11-09T16:03:02+00:00-DE-stock.csv
2023-11-09T16:03:17+00:00-AT-stock.csv
2023-11-09T16:04:44+00:00-EU-stock.csv

如果存在来自同一仓库的多个文件,则必须处理最新的文件。

文件始终是完全导出。

文件包含产品ID(字符串)以及生成导出文件的仓库中该产品的当前可用数量。

product_id;quantity
A1000;50
B3009;80
A8771;158

该文件必须由我们的应用程序处理。内容必须保存在我们稍后用于查询当前可用库存的数据库中。在我们的数据库中保存WarehouseId、ProductId和Quantity就足够了。您在此处选择的数据模型由您决定。

已处理的文件必须移动到另一个目录。为简化起见,我们不关心清理或导入失败。

├── new
│   ├── 2023-11-09T16:03:17+00:00-AT-stock.csv
│   └── 2023-11-09T16:04:44+00:00-EU-stock.csv
└── processed
    ├── 2023-11-09T15:02:17+00:00-CH-stock.csv
    ├── 2023-11-09T16:01:14+00:00-CH-stock.csv
    └── 2023-11-09T16:03:02+00:00-DE-stock.csv

目标 任务1的目标是我们的Go应用程序在预定义目录中搜索新的CSV文件。文件被处理,内容保存在我们的数据库中。

任务 3 - 跨国/州交付时间 我们拥有从每个仓库到其服务的每个符合条件的国家和州的平均交付时间的精确数据。此信息通过每个仓库生成的CSV导出提供。导出文件保存在与任务2相同的集中存储中。文件将保存在同一目录中。

例如:我们在德国有一个仓库——如果某个产品缺货,我们可以回退到欧洲仓库从那里为客户供货,但我们不能回退到我们的美国仓库。

我们需要开发的应用程序将从本地文件系统读取CSV文件。所有文件将保存在一个目录中。

├── new
│   ├── 2023-11-09T16:00:10+00:00-CH-delivery.csv
│   ├── 2023-11-09T16:00:20+00:00-DE-delivery.csv
│   ├── 2023-11-09T16:00:37+00:00-EU-delivery.csv
│   ├── 2023-11-09T16:01:56+00:00-AT-delivery.csv
│   ├── 2023-11-09T16:03:17+00:00-AT-stock.csv
│   └── 2023-11-09T16:04:44+00:00-EU-stock.csv
└── processed
    ├── 2023-11-09T15:02:17+00:00-CH-stock.csv
    ├── 2023-11-09T16:01:14+00:00-CH-stock.csv
    └── 2023-11-09T16:03:02+00:00-DE-stock.csv

交付时间文件将保存在与任务2中处理的库存文件相同的目录中。

每个仓库创建的文件名具有以下结构: Time[RFC-3339, ISO_8601]-WarehouseId-delivery.csv

$ ls -a | sort
2023-11-09T16:00:10+00:00-CH-delivery.csv
2023-11-09T16:00:20+00:00-DE-delivery.csv
2023-11-09T16:00:37+00:00-EU-delivery.csv
2023-11-09T16:01:56+00:00-AT-delivery.csv

如果存在来自同一仓库的多个文件,则必须处理最新的文件。

文件始终是完全导出。

文件包含国家ISO代码、州以及不考虑产品的平均交付时间(以天为单位)。

// 2023-11-09T16:00:10+00:00-CH-delivery.csv
country;state;delivery_time
CH;;3
DE;BW;4

此处显示的文件由瑞士仓库生成。瑞士(CH)的平均交付时间为3天。尽管如此,产品也可以交付到德国,但仅限于巴登-符腾堡州,这需要4天。如果指定了州,则意味着我们只交付到该特定州。文件中未出现的所有国家或所有州,此仓库不提供服务。如果未给出州,则意味着我们为整个国家提供服务。根据仓库的位置,某些州的交付时间可能低于该国其他地区。因此,我们可以在同一文件中同时有整个国家的条目以及国家和州的条目。

该文件必须由我们的应用程序处理。内容必须保存在我们稍后需要查询当前交付时间的数据库中。

已处理的文件必须移动到另一个目录。为简化起见,我们不关心清理或导入失败。

目标 处理提供的文件,将内容存储在我们的数据库中。

任务 4 - 库存和交付时间 API 客户为每个国家和地区运营的在线商店是隔离的。这意味着他们的数据库以及关于当前库存和交付时间的知识并未保存在集中式数据库中。每个商店必须单独处理上述文件导入,这导致每个系统每小时承受高负载。此外,由于每个商店使用自己的事实来源,可能会销售超过可用数量的产品,甚至宣传错误的交付时间。

我们现在希望在所有在线商店应用程序中移除库存和交付时间导入的处理,并用对我们Golang应用程序的API调用来替代。您的API调用的结果将显示在购物车中。在那里,我们显示每个产品的计算交付时间和可用库存。

为此,我们必须提供一个由在线商店应用程序使用的新API端点。为简化起见,我们不关心身份验证。

API端点必须接受以下参数:

  • 产品ID + 数量:我们提交想要了解当前库存和交付时间的产品。
  • 客户国家:由于交付时间取决于国家,我们将其作为上下文的一部分提交。
  • 客户州:由于交付时间可能取决于州,我们将其作为上下文的一部分提交。
{
  "products": {
    "A1000": 20,
    "B3009": 1200
  },
  "context": {
    "country": "DE",
    "state": "BY"
  }
}

此API调用的结果应包含我们在请求中提交的产品编号,以及每个产品在所需仓库中的可用库存信息。应根据可用库存和最短交付时间选择仓库。例如,如果我们可以从DE和EU仓库供货,我们首先选择交付时间最短的仓库,并补足剩余数量。

{
  "products": {
    "A1000": [
      {"warehouse": "DE", "quantity": "5", "delivery_time": 1},
      {"warehouse": "EU", "quantity": "15", "delivery_time": 8}
    ],
    "B3009": [
      {"warehouse": "DE", "quantity": "110", "delivery_time": 1}
    ]
  }
}

作为结果,我们收到每个产品的可能数量和交付时间信息。

目标

  • 我们有一个接受上述负载的API端点。
  • 根据先前导入的数据计算交付时间。
  • 以上述所示结果进行响应。
  • 不需要身份验证。

任务 5 - 预订 API 在实施库存API后,客户在错误库存或交付时间方面面临的问题减少了,但对于少量客户问题仍然存在。

为了防止这种情况,我们希望引入预留、确认和释放API端点。

预留 预订API由在线商店在结账过程中调用。如果客户导航到付款前的最后一个结账步骤,在线商店将调用预留API端点。负载将与库存和交付时间API完全相同。

{
  "products": {
    "A1000": 20,
    "B3009": 1200
  },
  "context": {
    "country": "DE",
    "state": "BY"
  }
}

预订将执行库存和交付时间查找,并将在每个所需仓库中预留库存。

我们的API必须像以前一样响应可用库存和可能的交付时间。现在,我们额外收到一个预订ID。此ID可用于确认和释放预订。

{
  "id": "59e57f14-be24-43c4-8a8e-4c22f5f2154e",
  "products": {
    "A1000": [
      {"warehouse": "DE", "quantity": "5", "delivery_time": 1},
      {"warehouse": "EU", "quantity": "15", "delivery_time": 8}
    ],
    "B3009": [
      {"warehouse": "DE", "quantity": "110", "delivery_time": 1}
    ]
  }
}

中止 如果客户未在其中一个在线商店完成结账过程,在线商店将调用中止API端点。这将删除先前创建的预订。

以下负载提交到此端点:

{
  "id": "59e57f14-be24-43c4-8a8e-4c22f5f2154e"
}

由于此API调用没有结果,我们的响应将是204无内容。

确认 如果客户完成结账和付款过程,在线商店现在将调用确认API端点,包括先前给出的预订ID。在此请求之后,关于预留库存的信息将永久保留在我们的数据库中。

{
  "id": "59e57f14-be24-43c4-8a8e-4c22f5f2154e"
}

由于此API调用没有结果,我们的响应将是204无内容。

自动释放 如果预订在一天内未被释放或确认,我们的应用程序必须自动从数据库中删除预留的库存。

订单处理 在线商店将订单提交到集中式订单管理系统。在线商店包含我们的预留API端点提供的信息。订单管理系统现在将根据我们的API提供的信息交付产品。这防止了销售超过可用数量的产品或显示错误的交付时间。

如果订单管理系统正在处理订单,仓库中的库存会发生变化。我们将在下一次文件导出时收到新的库存信息。为了释放预留的库存,订单管理系统将调用我们应用程序的一个API端点(释放API)。负载包括我们在预订期间创建的预订ID。

{
  "id": "59e57f14-be24-43c4-8a8e-4c22f5f2154e"
}

由于此API调用没有结果,我们的响应将是204无内容。

目标

  • 创建4个API端点(预留、中止、确认、释放)。
  • 预订应锁定特定仓库中的特定数量。
  • 所有对我们库存和交付时间API或预留API的进一步API调用都必须使用此信息。

更多关于Golang开发工程师求职经验分享的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

很高兴看到也有人为求职制作示例代码 ;)

我的一点建议: 我推荐使用 cobra,这样就能有一个 main.go 文件和一个二进制文件。

GitHub - spf13/cobra: A Commander for modern Go CLI interactions

我非常喜欢用 goose 和 sqlc 来处理数据库模式和查询:

GitHub - pressly/goose: A database migration tool. Supports SQL migrations...

GitHub - sqlc-dev/sqlc: Generate type-safe code from SQL

最后是来自 @Dean_Davidson 的:

GitHub - DeanPDX/dotconfig: Simplify configuration of microservices.

(你现在还没有 .env 文件,但一个真正的应用会需要这个。)

祝你好运!

更多关于Golang开发工程师求职经验分享的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


技术方案分析

你的解决方案展示了完整的库存管理系统架构,以下从Go语言实现角度分析关键技术点:

1. 并发文件处理

// 使用goroutine池处理CSV文件
func processFiles(dir string, db *sql.DB) error {
    files, err := os.ReadDir(dir)
    if err != nil {
        return err
    }
    
    sem := make(chan struct{}, 10) // 限制并发数
    var wg sync.WaitGroup
    
    for _, file := range files {
        if strings.HasSuffix(file.Name(), ".csv") {
            wg.Add(1)
            go func(filename string) {
                defer wg.Done()
                sem <- struct{}{}
                defer func() { <-sem }()
                
                if err := processSingleFile(filename, db); err != nil {
                    log.Printf("处理文件 %s 失败: %v", filename, err)
                }
            }(file.Name())
        }
    }
    
    wg.Wait()
    return nil
}

2. 批量数据库插入优化

// 使用事务批量插入提高性能
func bulkInsertStock(tx *sql.Tx, warehouseID string, records []StockRecord) error {
    stmt, err := tx.Prepare(`
        INSERT INTO stock (warehouse_id, product_id, quantity, updated_at) 
        VALUES ($1, $2, $3, NOW())
        ON CONFLICT (warehouse_id, product_id) 
        DO UPDATE SET quantity = EXCLUDED.quantity, updated_at = NOW()
    `)
    if err != nil {
        return err
    }
    defer stmt.Close()
    
    for _, record := range records {
        if _, err := stmt.Exec(warehouseID, record.ProductID, record.Quantity); err != nil {
            return err
        }
    }
    return nil
}

3. 库存分配算法

// 基于最短交付时间的库存分配
func allocateStock(productID string, quantity int, country, state string, db *sql.DB) ([]Allocation, error) {
    query := `
        SELECT s.warehouse_id, s.quantity, d.delivery_time
        FROM stock s
        JOIN delivery_times d ON s.warehouse_id = d.warehouse_id
        WHERE s.product_id = $1 
        AND d.country = $2 
        AND (d.state = $3 OR d.state = '')
        AND s.quantity > 0
        ORDER BY d.delivery_time ASC
    `
    
    rows, err := db.Query(query, productID, country, state)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var allocations []Allocation
    remaining := quantity
    
    for rows.Next() && remaining > 0 {
        var alloc Allocation
        if err := rows.Scan(&alloc.Warehouse, &alloc.Available, &alloc.DeliveryTime); err != nil {
            return nil, err
        }
        
        allocated := min(alloc.Available, remaining)
        if allocated > 0 {
            alloc.Quantity = allocated
            allocations = append(allocations, alloc)
            remaining -= allocated
        }
    }
    
    if remaining > 0 {
        return nil, fmt.Errorf("库存不足: 产品 %s 需要 %d, 仅分配 %d", 
            productID, quantity, quantity-remaining)
    }
    
    return allocations, nil
}

4. 预留系统实现

// 使用数据库事务确保预留原子性
type ReservationService struct {
    db *sql.DB
}

func (s *ReservationService) CreateReservation(req ReservationRequest) (*ReservationResponse, error) {
    tx, err := s.db.Begin()
    if err != nil {
        return nil, err
    }
    defer tx.Rollback()
    
    reservationID := uuid.New().String()
    
    // 为每个产品预留库存
    for productID, quantity := range req.Products {
        allocations, err := s.allocateWithLock(tx, productID, quantity, req.Context)
        if err != nil {
            return nil, err
        }
        
        // 保存预留记录
        if err := s.saveReservation(tx, reservationID, productID, allocations); err != nil {
            return nil, err
        }
    }
    
    if err := tx.Commit(); err != nil {
        return nil, err
    }
    
    return &ReservationResponse{
        ID:       reservationID,
        Products: s.buildResponse(allocations),
    }, nil
}

func (s *ReservationService) allocateWithLock(tx *sql.Tx, productID string, quantity int, ctx Context) ([]Allocation, error) {
    // 使用SELECT FOR UPDATE锁定库存记录
    rows, err := tx.Query(`
        SELECT warehouse_id, quantity 
        FROM stock 
        WHERE product_id = $1 AND quantity > 0
        FOR UPDATE SKIP LOCKED
    `, productID)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    // 分配逻辑...
}

5. 自动释放定时任务

// 使用time.Ticker实现定时清理
func (s *ReservationService) StartAutoRelease(interval time.Duration) {
    ticker := time.NewTicker(interval)
    go func() {
        for range ticker.C {
            if err := s.releaseExpiredReservations(); err != nil {
                log.Printf("自动释放失败: %v", err)
            }
        }
    }()
}

func (s *ReservationService) releaseExpiredReservations() error {
    _, err := s.db.Exec(`
        DELETE FROM reservations 
        WHERE created_at < NOW() - INTERVAL '24 hours' 
        AND status = 'pending'
    `)
    return err
}

6. API路由设计

func setupRoutes(r *gin.Engine, db *sql.DB) {
    api := r.Group("/api/v1")
    {
        api.POST("/stock/query", handleStockQuery)
        api.POST("/reservations", handleCreateReservation)
        api.DELETE("/reservations/:id", handleCancelReservation)
        api.PUT("/reservations/:id/confirm", handleConfirmReservation)
        api.PUT("/reservations/:id/release", handleReleaseReservation)
    }
}

func handleStockQuery(c *gin.Context) {
    var req StockRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    result, err := queryStock(req, db)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(200, result)
}

7. 性能优化建议

  • 使用连接池:db.SetMaxOpenConns(100)
  • 实现查询缓存:对热点产品使用Redis缓存
  • 使用gin的中间件进行请求限流
  • 实现Prometheus监控指标

这个方案展示了Go在并发处理、API设计和数据库操作方面的优势。代码结构清晰,错误处理完善,适合生产环境部署。

回到顶部