Golang实现分钟级转小时级数据:聚合求和与均值计算问题
问题描述
我有一张存储分钟级数据的数据库表,需要按小时对数据做聚合:求和相关字段,同时计算PH、COD列的均值。自己编写了Golang函数,但运行后出现数据重复问题,不确定是代码逻辑还是实现出错。因担心服务器开销,不想用原生SQL,希望通过逐行累加生成小时级新记录,同时算出PH、COD的均值。
代码示例
func (server *Server) DataPerHour(w http.ResponseWriter, r *http.Request) { var PH, COD float64 var countEntry float64 sparingData := models.SparingReport{} sparingHourData := models.SparingReportHour{} countsparingdatalast, _ := sparingData.CountLastSparingReportHourByID(server.DB, 2) if *countsparingdatalast > 0 { sparingDataPerHour, _ := sparingData.FindAllSparingReport(server.DB) if len(*sparingDataPerHour) > 0 { startHour := (*sparingDataPerHour)[0].RecordedAt.Hour() for _, dataReceived := range *sparingDataPerHour { nowHour := dataReceived.RecordedAt.Hour() if startHour == nowHour { if dataReceived.PH > 0 { PH += dataReceived.PH } if dataReceived.COD > 0 { COD += dataReceived.COD } countEntry++ } else if startHour != nowHour { sparingHourData.Prepare() sparingHourData.ID = dataReceived.ID sparingHourData.RecordedAt = dataReceived.RecordedAt if countEntry > 0 { sparingHourData.PH = PH / countEntry sparingHourData.COD = COD / countEntry } sparingHourData.Prepare() sparingHourData.IsProcess = true _, _ = sparingHourData.SaveSparingReportHour(server.DB) PH = 0 COD = 0 countEntry = 0 startHour = nowHour } } } } }
当前输出

预期输出
| ID | RecordedAt | PH | COD |
|---|---|---|---|
| 1 | 2023-11-17 19:00:00+00 | 7.1 | 25.5 |
| 2 | 2023-11-17 20:00:00+00 | 7.8 | 24.7 |
问题分析与修复方案
你的代码存在几个核心逻辑问题,导致数据重复或不符合预期:
- 未处理最后一小时的数据:循环结束后,最后一组累加的小时数据没有写入数据库
- 仅按小时分组,忽略日期:跨天的同一小时(如11月17日23点和11月18日0点)会被错误区分或合并
- 时间未对齐到整点:直接使用原数据的
RecordedAt,导致小时级记录时间不是整点 - 结构体复用导致数据残留:循环中复用同一个
sparingHourData实例,若Prepare()未完全重置字段,会携带旧数据 - ID赋值错误:直接复用原分钟级数据的ID,导致小时级记录ID重复
- 错误处理缺失:忽略所有错误,无法排查数据读写失败的问题
修复后的代码
import ( "log" "net/http" "time" ) func (server *Server) DataPerHour(w http.ResponseWriter, r *http.Request) { var PH, COD float64 var countEntry int sparingData := models.SparingReport{} // 计数查询添加错误处理 countsparingdatalast, err := sparingData.CountLastSparingReportHourByID(server.DB, 2) if err != nil { http.Error(w, "获取小时数据计数失败", http.StatusInternalServerError) return } if *countsparingdatalast > 0 { sparingDataPerHour, err := sparingData.FindAllSparingReport(server.DB) if err != nil { http.Error(w, "获取分钟数据失败", http.StatusInternalServerError) return } if len(*sparingDataPerHour) == 0 { return } // 用日期+小时作为分组键,解决跨天问题 currentGroup := getHourGroup((*sparingDataPerHour)[0].RecordedAt) for _, dataReceived := range *sparingDataPerHour { dataGroup := getHourGroup(dataReceived.RecordedAt) if dataGroup == currentGroup { // 累加有效数据,单独判断避免无效值影响均值 if dataReceived.PH > 0 { PH += dataReceived.PH countEntry++ } if dataReceived.COD > 0 { COD += dataReceived.COD } } else { // 写入上一小时的聚合数据 saveHourData(currentGroup, PH, COD, countEntry, server.DB) // 重置累加器 PH = 0 COD = 0 countEntry = 0 currentGroup = dataGroup // 处理当前数据的首次累加 if dataReceived.PH > 0 { PH += dataReceived.PH countEntry++ } if dataReceived.COD > 0 { COD += dataReceived.COD } } } // 处理最后一小时的数据 saveHourData(currentGroup, PH, COD, countEntry, server.DB) } } // 定义分组键,区分不同日期的同一小时 type hourGroup struct { year int month time.Month day int hour int } // 生成时间对应的分组键 func getHourGroup(t time.Time) hourGroup { t = t.UTC() // 统一使用UTC时间,避免时区差异问题 return hourGroup{ year: t.Year(), month: t.Month(), day: t.Day(), hour: t.Hour(), } } // 封装小时数据保存逻辑 func saveHourData(group hourGroup, PH, COD float64, count int, db *gorm.DB) { if count == 0 { return } // 每次创建新实例,避免数据残留 sparingHourData := models.SparingReportHour{} sparingHourData.Prepare() // 生成整点时间 sparingHourData.RecordedAt = time.Date(group.year, group.month, group.day, group.hour, 0, 0, 0, time.UTC) sparingHourData.PH = PH / float64(count) sparingHourData.COD = COD / float64(count) sparingHourData.IsProcess = true // 保存时处理错误 if _, err := sparingHourData.SaveSparingReportHour(db); err != nil { log.Printf("保存小时数据失败: %v", err) } }
关键修复点说明
- 跨天分组处理:用日期+小时作为分组依据,避免不同日期的同一小时被错误合并
- 最后一小时数据处理:循环结束后单独处理最后一组累加数据
- 时间对齐:生成严格的整点时间作为小时级记录的
RecordedAt - 结构体实例隔离:每次保存时创建新的
sparingHourData实例,避免旧数据残留 - 错误处理:添加HTTP错误返回和日志记录,方便排查问题
- ID逻辑修正:移除手动赋值ID的代码,依赖数据库自增逻辑(若表结构支持)
内容的提问来源于stack exchange,提问作者Cybercand
相关产品推荐
相关产品推荐

