笔者已经使用copilot协助开发有一段时间了,但一直没有总结一个协助代码开发的案例,特别是怎么问copilot,按照什么顺序问,哪些方面可以高效的生成需要的代码,这一次,笔者以IP解析需求为例,沉淀一个实践案例,供大家参考
当然,其实也不局限于copilot本身,类似的VsCode插件有很多,本文也只是拿chat的AI大模型做例子,只要是deepseek-v3就好
为了聚焦,具体需求做了些抽象,简单的说,需要对系统一个IP解析功能进行更新:
这一步很重要,因为很多时候当我们拿到需求文档的时候,希望直接给到IDE的AI助手,结果一般事与愿违,因为AI助手适合在一个限定范围内学习和给出高质量意见
所以要做一些简单前期工作——目的是让copilot学习尽量少的代码资料,从而减少幻觉的输出
最好把三个库加载的代码先尽量写到一个文件中,比如:
func init() {
slog.Info("loading A.dat file")
if info, err := os.Stat("A.dat"); err == nil && !info.IsDir() {
slog.Info("A.dat file loading from /data/A.dat")
tobdb, err = ipcity.LoadV2("/data/A.dat")
if err != nil {
slog.Error("load /data/A.dat file fail")
panic("cannot initialize A data")
}
}
slog.Info("loading B.dat file")
if info, err := os.Stat("B.dat"); err == nil && !info.IsDir() {
slog.Info("B.dat file loading from /data/B.dat")
tobdb, err = ipcity.LoadV2("/data/B.dat")
if err != nil {
slog.Error("load /data/B.dat file fail")
panic("cannot initialize B data")
}
}
slog.Info("loading C.dat file")
if info, err := os.Stat("C.dat"); err == nil && !info.IsDir() {
slog.Info("C.dat file loading from /data/C.dat")
tobdb, err = ipcity.LoadV2("/data/C.dat")
if err != nil {
slog.Error("load /data/C.dat file fail")
panic("cannot initialize C data")
}
}
}
这里有两个注意的点:
(1) 以终为始:
我们是希望copilot能够准确的学习到这是三个库的加载方法,所以这里要写的教条一点:即三个函数相似度要高,而且要通过注释、日志反复增强对函数作用的说明,这样copilot会更准确的学习这里的业务逻辑
(2)日积月累:
如果前期代码就是这样“教条”的撰写风格,那么这段代码本身就可以用copilot生成
IP解析函数中,要包含对A、B库的调用及综合算法
因为涉及综合算法,最好把综合算法放到一个文件中,这样copilot就可以读更少的文件
然后把确定输入代码的地方,写个注释,表示要在这里写
比如:
// ParseIP parse passed in ip string and return country, region, city, isp.
func ParseIP(clientIP string) (ip, country, region, city, isp string) {
ip = clientIP
switch IPLibraryVersion {
case "v1":
、、、
case "v2":
// patch result from C
// patch result from A
resultA := A.Search(net.ParseIP(clientIP))
if meta != nil {
country = meta.Country()
region = meta.Province()
city = meta.City()
isp = meta.ISP()
}
// patch result from B, espicially for ISP
resultB := B.Search(net.ParseIP(clientIP))
if resultB != nil {
if country == "" || country == "未知" { // if cannot found country from A, then turn to B
country = resultB.Country()
region = resultB.Province()
city = resultB.City()
isp = resultB.ISP()
} else if (strings.HasPrefix(isp, "Error") && (B.ISP() != "未知" && B.ISP() != "")) || isp == "未知" || isp == "" { // if B's ISP is prefix with 'Error', so replace it with tobdb's ISP
isp = B.ISP()
}
}
这里也有两个注意的点:
(1)写好注释:
一般来说,写注释是研发同学最难以为继的事情,但随着copilot的到来,大家可以写完一个函数后,让copilot帮忙写注释,对于研发同学来说,甚至只需要输入“//”,然后等待copilot生成就好
(2)变量简单命名:
除了注释,变量名的清晰明了也是可以让copilot来更好的学习,同时,这里最好写的比较有规律,比如resultA、resultB,这样剩下的变量名也可以自助生成
这里Copilot类似工具有很多,笔者用的是VSCode的IDE,大家可以随意选择,本质上是DeepSeek-v3模型就好
这一步就需要把需求文档的内容,进行输入,当然,很多时候,需求充满着未添加的背景信息和口语的表述,作为一名研发,有时需要做一些逻辑转换用语
这里可以看一个业务逻辑输入示例:
我想写一段逻辑,现在有三种数据源获取了country,region,city,isp四个数据,我希望A库的数据优先级最高,只有当A库的country识别不出来,或者country识别出来,但region的识别不出来的时候,才使用B库的数据;然后只有B库识别isp为“Error”打头时或者为空或者为“未知”,且C库识别isp不为空或者“未知”,才使用C库数据,如果三者都识别不出来,ISP如果有英文则用英文的版本,否则用A;其中一个重要逻辑是,最后使用的country,region,city,isp必须整体使用某一个数据源,且如果region识别为国内的广东等省必须加省后缀...
这是一个失败的表述,虽然可能完全来自与需求文档/会议结论/邮件输出,但对于copilot来说,逻辑十分的混乱
我们来分析下为什么混乱:
!17 “看到这里,很多读者可能也觉得这个逻辑十分混乱,但笔者想说的是,这是一个真实的业务内容,之所以大家觉得逻辑混乱是因为,撰写的人具备大量的背景知识,所以撰写者觉得清晰明了,但其他人和copilot完全不知所云!”!
那怎么才能写出清晰的业务逻辑输入呢?
笔者认为:清晰的逻辑输入需要清晰的输入格式
我重新梳理一下规则,1,代码实现在ParseIP的v2逻辑路径中;2.三个数据源分别来自resultA、resultB、resultC,最终要选择某一个的数据源来获取country,region,city,四个数据,3.第一优先级是获取resultA的数据,当resultA分析的country为空/未知或者region为空/未知时,才用resultB数据;4.第二优先级是获取resultB数据,当resultB分析的country为空/未知或者region为空/未知时,才使用resultC数据;5.如果三者都识别不出来,ISP如果有英文则用英文的版本,否则用A,帮我写下这个代码
这里对上述的问题进行了修正,同时增加几点优化:
我们看下结果:
case "v2":
// 从三个数据源获取信息
resultA := A.Search(net.ParseIP(clientIP))
resultB := B.Search(net.ParseIP(clientIP))
resultC := C.Search(net.ParseIP(clientIP))
// 定义最终使用的数据源
var finalSource *IPInfo
// 第一优先级:resultA
if resultA != nil && resultA.Country() != "" && resultA.Country() != "未知" &&
resultA.Province() != "" && resultA.Province() != "未知" {
finalSource = resultA
} else {
// 第二优先级:resultB
if resultB != nil && resultB.Country() != "" && resultB.Country() != "未知" &&
resultB.Province() != "" && resultB.Province() != "未知" {
finalSource = resultB
} else {
// 第三优先级:resultC
if resultC != nil && resultC.Country() != "" && resultC.Country() != "未知" &&
resultC.Province() != "" && resultC.Province() != "未知" {
finalSource = resultB
} else {
// 如果metaPatch也不满足条件,回退到resultC
finalSource = resultA
}
}
}
// 处理ISP的特殊情况
if finalSource == resultB && (resultB.ISP() == "" || resultB.ISP() == "未知" || strings.HasPrefix(resultB.ISP(), "Error")) {
if resultC != nil && resultC.ISP() != "" && resultC.ISP() != "未知" {
isp = resultC.ISP()
} else {
isp = resultB.ISP()
}
} else {
isp = finalSource.ISP()
}
// 获取最终结果
if finalSource != nil {
country = finalSource.Country()
region = finalSource.Province()
city = finalSource.City()
if isp == "" {
isp = finalSource.ISP()
}
}
我们可以看到效果还是比较明显的:
通过刚才的表述,我们生成了一个“基础”的逻辑代码
这个时候,我们可能会发现一些问题,并对源码进行了部分修正
比如笔者修改了下逻辑:
// 处理ISP的特殊情况
if finalSource == metaCDN && (metaCDN.ISP() == "" || metaCDN.ISP() == "未知" || strings.HasPrefix(metaCDN.ISP(), "Error")) {
if metaToB != nil && metaToB.ISP() != "" && metaToB.ISP() != "未知" {
finalSource = metaToB
}
}
可以看到,在这里做ISP的选择扩大到整个数据源,即完成第一次描述的“最后使用的country,region,city,isp必须整体使用某一个数据源”的逻辑;同时最命名进行了优化,毕竟使用A、B、C这样的命名对于研发同学来说还是不太友好
修改完毕后,我们需要copilot帮我们对比下自动生成的代码和人工修改的的区别,同时也可以验证我们修改的逻辑是否“正确”
比如下面的这段话:
我修改好了,帮我看看我写的和你写的区别,并从代码质量、可读性、性能等角度分析下不同
copilot输出如下:
从代码质量、可读性和性能等角度分析,你的代码和我的代码主要有以下区别:
1. 代码质量:
- 你的代码在变量命名上更清晰,使用了 `CommercialIPLibrary` 和 `OpenSoueceIPLibrary` 这样的命名,比我的 `resultA` 和 `resultB` 更直观地表达了数据源的性质。
- 我的代码在处理 ISP 时更细致,保留了原始数据源的 ISP 信息,而你的代码在 ISP 不满足条件时直接切换了整个数据源,这可能丢失其他字段的信息。
2. 可读性:
- 你的代码结构更简洁,将数据源选择逻辑集中在一个地方,便于理解。
- 我的代码在 ISP 处理部分增加了额外的逻辑,虽然更符合需求,但也增加了代码的复杂度。
- 你的代码缺少注释,而我的代码有更详细的注释说明,这有助于后续维护。
3. 性能:
- 两者性能差异不大,都只进行了一次数据源查询和简单的条件判断。
- 你的代码在 ISP 不满足条件时直接切换数据源,可能会减少一次 ISP 的获取操作,略微提升性能。
- 我的代码在处理 ISP 时保留了原始数据源的其他信息,可能需要额外的内存开销。
4. 功能完整性:
- 我的代码更严格地遵循了你最初的需求描述,特别是在 ISP 处理逻辑上。
- 你的代码在 ISP 不满足条件时直接切换整个数据源,这可能导致 country、region、city 等信息也被替换,可能不符合需求。
5. 健壮性:
- 我的代码在获取最终结果时增加了 `if isp == ""` 的判断,确保 ISP 一定有值,更健壮。
- 你的代码在切换数据源时没有处理可能为 nil 的情况,可能存在潜在风险。
建议改进:
1. 在 ISP 处理逻辑上,可以保留原始数据源的其他信息,只替换 ISP 字段。
2. 增加必要的注释,特别是数据源选择逻辑和 ISP 处理逻辑。
3. 在切换数据源时增加 nil 检查,提高代码健壮性。
示例改进代码:
// 处理ISP的特殊情况
if finalSource == OpenSoueceIPLibrary && (OpenSoueceIPLibrary.ISP() == "" || OpenSoueceIPLibrary.ISP() == "未知" || strings.HasPrefix(OpenSoueceIPLibrary.ISP(), "Error")) {
if InternIPLibrary != nil && InternIPLibrary.ISP() != "" && InternIPLibrary.ISP() != "未知" {
// 只替换ISP,保留其他信息
isp = InternIPLibrary.ISP()
} else {
isp = OpenSoueceIPLibrary.ISP()
}
} else {
isp = finalSource.ISP()
}
总的来说,你的代码在简洁性和可读性上做得更好,但在功能完整性和健壮性上还有提升空间。建议结合两者的优点,在保持代码简洁的同时,确保功能完整和健壮。
可以看到copilot输出,验证修改的地方的逻辑合理性,当然copilot除了例行夸奖以外,仍然强烈建议了自己在ISP获取的优点,说明,从研发角度,前者通常意义上更“好”一些,这里我们选择采纳,同时可以提出了一个懒得核实的小疑问
“判空处理”一般来说是一个标准的“脏货累活”,因为需要花一段时间来走查代码确定,相似的还有“错误处理”,这里可以交给“copilot”来做
我按照你的建议修改了,但我有个疑问44行-46行,需要做判空处理吗
看看copilot的输出
关于44行-46行的判空处理问题,我的建议如下:
1. **44行**:`country = finalSource.Country()`
- 需要判空处理,因为即使 `finalSource` 不为 nil,`Country()` 方法返回的值可能为空字符串。
- 建议改为:`if finalSource.Country() != "" { country = finalSource.Country() }`
2. **45行**:`region = finalSource.Province()`
- 同样需要判空处理,原因同上。
- 建议改为:`if finalSource.Province() != "" { region = finalSource.Province() }`
3. **46行**:`city = finalSource.City()`
- 需要判空处理,原因同上。
- 建议改为:`if finalSource.City() != "" { city = finalSource.City() }`
4. **47行**:`isp = finalSource.ISP()`
- 这里不需要额外判空,因为在前面的 ISP 处理逻辑中已经确保了 isp 有值。
修改后的代码建议:
// 获取最终结果
if finalSource != nil {
if finalSource.Country() != "" {
country = finalSource.Country()
}
if finalSource.Province() != "" {
region = finalSource.Province()
}
if finalSource.City() != "" {
city = finalSource.City()
}
// isp 已经在前面处理过,这里不需要再次判空
isp = finalSource.ISP()
}
这样修改的原因:
1. 防止空字符串覆盖之前可能已经设置的有效值
2. 保持数据一致性,避免出现部分字段为空的情况
3. 符合 Go 语言的防御性编程原则
4. 提高代码的健壮性,避免潜在的运行时错误
虽然这些判空处理可能会增加一些代码量,但它们能有效防止数据丢失和异常情况,是值得的。
可以看到,copilot只需要几秒钟就走查完了,这里还额外赠送了47行,ISP不做判空的解释,省去了走查代码的时间
看看最后生成的代码:
case "v2":
// 从三个数据源获取信息
CommercialIPLibraryResult := CommercialIPLibrary.Search(net.ParseIP(clientIP))
OpenSoueceIPLibraryResult := OpenSoueceIPLibrary.Search(net.ParseIP(clientIP))
InternIPLibraryResult := InternIPLibrary.Search(net.ParseIP(clientIP))
// 定义最终使用的数据源
var finalSource *IPInfo
// 第一优先级:CommercialIPLibraryResult
if CommercialIPLibraryResult != nil && CommercialIPLibraryResult.Country() != "" && CommercialIPLibraryResult.Country() != "未知" &&
CommercialIPLibraryResult.Province() != "" && CommercialIPLibraryResult.Province() != "未知" {
finalSource = OpenSoueceIPLibraryResult
} else {
// 第二优先级:OpenSoueceIPLibraryResult
if OpenSoueceIPLibraryResult != nil && OpenSoueceIPLibraryResult.Country() != "" && OpenSoueceIPLibraryResult.Country() != "未知" &&
OpenSoueceIPLibraryResult.Province() != "" && OpenSoueceIPLibraryResult.Province() != "未知" {
finalSource = InternIPLibraryResult
} else {
// 第三优先级:InternIPLibraryResult
if InternIPLibraryResult != nil && InternIPLibraryResult.Country() != "" && InternIPLibraryResult.Country() != "未知" &&
InternIPLibraryResult.Province() != "" && InternIPLibraryResult.Province() != "未知" {
finalSource = OpenSoueceIPLibraryResult
} else {
// 如果metaPatch也不满足条件,回退到CommercialIPLibraryResult
finalSource = CommercialIPLibraryResult
}
}
}
// 处理ISP的特殊情况
if finalSource == OpenSoueceIPLibraryResult && (OpenSoueceIPLibraryResult.ISP() == "" || OpenSoueceIPLibraryResult.ISP() == "未知" || strings.HasPrefix(OpenSoueceIPLibraryResult.ISP(), "Error")) {
if InternIPLibraryResult != nil && InternIPLibraryResult.ISP() != "" && InternIPLibraryResult.ISP() != "未知" {
isp = InternIPLibraryResult.ISP()
} else {
isp = OpenSoueceIPLibraryResult.ISP()
}
} else {
isp = finalSource.ISP()
}
// 获取最终结果
if finalSource != nil {
if finalSource.Country() != "" {
country = finalSource.Country()
}
if finalSource.Province() != "" {
region = finalSource.Province()
}
if finalSource.City() != "" {
city = finalSource.City()
}
// isp 已经在前面处理过,这里不需要再次判空
isp = finalSource.ISP()
}
到这里,业务逻辑基本上写完了,但大家可以也看到了,这个代码强逻辑,而且三个优先级的判断逻辑相似,似乎没有做“整合”
!24 别急,面向copilot编程就是需要先把业务逻辑搞对再优化代码结构——这是两个不同的步骤,不能省了略!
这里可以把不顺眼的地方着重提出来
基于我的逻辑,那这里还有没有比较好的逻辑推荐,也可以推荐性能、可用性、可读性、健壮性上的优化
看看copilot的输出:
基于你的代码逻辑,我从性能、可用性、可读性和健壮性方面提出以下优化建议:
1. **性能优化**
- 将重复的 `net.ParseIP(clientIP)` 提取到变量中,避免重复解析:
goipAddr := net.ParseIP(clientIP)
CommercialIPLibraryResult := A.Search(ipAddr)
OpenSoueceIPLibraryResult := B.Search(ipAddr)
InternIPLibraryResult := C.Search(ipAddr)
2. **可用性优化**
- 增加默认值处理,确保返回值不会为空:
gocountry = "未知"
region = "未知"
city = "未知"
isp = "未知"
3. **可读性优化**
- 将数据源选择逻辑提取为独立函数:
gofunc selectDataSource(CommercialIPLibraryResult, OpenSoueceIPLibraryResult, InternIPLibraryResult *ipcity) *ipcity {
// 数据源选择逻辑
}
4. **健壮性优化**
- 增加对输入参数的校验:
goif clientIP == "" {
return ip, country, region, city, isp
}
5. **代码重构建议**
- 使用结构体封装返回结果,提高代码可维护性:
gotype IPInfo struct {
IP string
Country string
Region string
City string
ISP string
}
这些优化建议可以帮助你:
1. 提高代码的可维护性和可读性
2. 增强代码的健壮性
3. 减少重复代码
4. 提高代码的复用性
5. 使错误处理更加完善
特别是将数据源选择和ISP处理逻辑提取为独立函数,可以使主逻辑更加清晰,也便于后续维护和测试。
这里copilot也给出了完整修改代码的样例:
func ParseIP(clientIP string) (ip, country, region, city, isp string) {
// 初始化默认值
ip = clientIP
country = "未知"
region = "未知"
city = "未知"
isp = "未知"
// 输入校验
if clientIP == "" {
return
}
// 解析IP地址
ipAddr := net.ParseIP(clientIP)
if ipAddr == nil {
return
}
switch IPLibraryVersion {
case "v1":
、、、
case "v2":
// 获取数据源
CommercialIPLibraryResult := A.Search(ipAddr)
OpenSoueceIPLibraryResult := B.Search(ipAddr)
InternIPLibraryResult := C.Search(ipAddr)
// 选择数据源
finalSource := selectDataSource(CommercialIPLibraryResult, OpenSoueceIPLibraryResult, InternIPLibraryResult)
if finalSource == nil {
return
}
// 处理ISP
isp = processISP(finalSource, OpenSoueceIPLibraryResult, InternIPLibraryResult)
// 设置最终结果
if finalSource.Country() != "" {
country = finalSource.Country()
}
if finalSource.Province() != "" {
region = finalSource.Province()
}
if finalSource.City() != "" {
city = finalSource.City()
}
default:
panic("invalid ip library version, and this is not expected! please set it to 'v1' or 'v2'")
}
return
}
// selectDataSource 选择合适的数据源
func selectDataSource(CommercialIPLibraryResult, OpenSoueceIPLibraryResult, InternIPLibraryResult *ipcity) *ipcity {
if meteIpip != nil && isValidLocation(CommercialIPLibraryResult) {
return CommercialIPLibraryResult
}
if metaCdn != nil && isValidLocation(OpenSoueceIPLibraryResult) {
return OpenSoueceIPLibraryResult
}
if metaToB != nil && isValidLocation(InternIPLibraryResult) {
return InternIPLibraryResult
}
return CommercialIPLibraryResult
}
// isValidLocation 检查位置信息是否有效
func isValidLocation(source *ipcity.Meta) bool {
return source.Country() != "" && source.Country() != "未知" &&
source.Province() != "" && source.Province() != "未知"
}
// processISP 处理ISP信息
func processISP(finalSource, metaCdn, metaToB *ipcity.Meta) string {
if finalSource == metaCdn && (metaCdn.ISP() == "" || metaCdn.ISP() == "未知" || strings.HasPrefix(metaCdn.ISP(), "Error")) {
if metaToB != nil && metaToB.ISP() != "" && metaToB.ISP() != "未知" {
return metaToB.ISP()
}
}
return finalSource.ISP()
}
可以看到:
逻辑整体清晰了之后,再添加一些特殊逻辑进来这样的话,可以让代码始终处在可读可控的状态
比如:
增加一个特殊逻辑,如果识别出来的country包含香港、澳门字样,需要增加中国前缀,如果region里面包含香港、澳门字样,需要把country固定设置为中国
copilot输出如下:
// ==== 原代码 ====
// ParseIPWrapper parse client ip for fasthttp framework
// ...省略其他代码...
// ==== 修改后代码 ====
// handleSpecialRegions 处理特殊地区逻辑
func handleSpecialRegions(country, region string) (string, string) {
// 定义特殊地区关键词
specialRegions := []string{"香港", "澳门"}
// 检查region是否包含特殊地区
for _, sr := range specialRegions {
if strings.Contains(region, sr) {
country = "中国"
break
}
}
// 检查country是否包含特殊地区
for _, sr := range specialRegions {
if strings.Contains(country, sr) {
country = "中国 " + country
break
}
}
return country, region
}
// ParseIPWrapper parse client ip for fasthttp framework
// ...省略其他代码...
可以看到
最终的代码在:
// 处理ISP
、、、
isp = processISP(finalSource, metaCdn, metaToB)
// 获取最终结果
if finalSource != nil {
if finalSource.Country() != "" {
country = finalSource.Country()
}
if finalSource.Province() != "" {
region = finalSource.Province()
}
if finalSource.City() != "" {
city = finalSource.City()
}
if finalSource.ISP() != "" {
isp = finalSource.ISP()
}
}
// 处理特殊地区
country, region = processSpecialRegions(country, region)
、、、
到这里,业务逻辑基本上写完了,当然了,一个高质量的代码还需要添加单元测试,这里很多时候研发同学都懒得写
但copilot写的话,就会比较快
当然,这里也容易出现的问题是,不让copilot分析代码,直接写单元测试,由于代码分析不太准,所以写的单元测试也不会太好
!24 所以步骤是:新开一个窗口,先让copilot代码分析,再生成测试用例!
新开一个窗口避免之前的信息成为脏数据,同时也double check下代码逻辑的准确性
帮忙分析下ParseIP函数的逻辑
看看copilot的输出:
我来分析一下`ParseIP`函数的逻辑:
1. 函数定义:
func ParseIP(clientIP string) (ip, country, region, city, isp string)
- 输入:客户端IP地址字符串
- 输出:返回5个字符串,分别是IP地址、国家、地区、城市和ISP(互联网服务提供商)
2. 初始化返回值:
ip = clientIP
country, region, city, isp = "unknown", "unknown", "unknown", "unknown"
- 将输入IP直接赋值给返回值ip
- 其他字段初始化为"unknown"
3. 根据IPLibraryVersion选择不同的解析方式:
goswitch IPLibraryVersion {
case "v1":
//
case "v2":
// 使用多个数据源解析IP
default:
panic("invalid ip library version")
}
4. v2版本解析逻辑:
- 首先将IP字符串转换为net.IP类型
- 使用三个不同的数据源(ipipdb、ipdb、tobdb)查询IP信息
- 通过`selectDataSource`函数选择最合适的数据源
- 处理ISP信息,优先使用ToB数据源的ISP信息
- 处理特殊地区(如香港、澳门等),确保这些地区被正确标记为中国的一部分
5. 返回值:
- 返回处理后的IP地址、国家、地区、城市和ISP信息
- 特别注意:对于台湾等地区,会确保其国家字段标记为"中国"
6. 其他注意事项:
- 该函数会处理IPv4地址,不支持IPv6
- 对于私有地址,会直接返回"unknown"
总的来说,这个函数的主要功能是根据不同的IP库版本,解析IP地址的地理位置信息,并确保特殊地区的正确标记。它采用了灵活的数据源选择和错误处理机制,以保证解析结果的准确性和可靠性。
这里其实可以对刚才的每个函数进行确认,比如:
详细说下func processSpecialRegions(country, region string) (string, string) 的作用,并结合它一起分析下v2的逻辑
这里笔者只演示一个函数的结果,
如果有问题就修正copilot的输出到正确
如果觉得copilot的输出没有问题了就开始生成测试用例
我需要写两类测试用例,1.一类是针对不同的IP库源单独一个测试用例,输入是一个Ip列表,输出是IP地址、国家、地区、城市和ISP,2.一类是针对ParseIP的测试用例,撰写在这个文件夹所在地方,ip_test.go文件中
这里注意的点:
package ip
import (
"testing"
)
// 测试不同IP库源
func TestIPSources(t *testing.T) {
tests := []struct {
name string
ip string
expected struct {
ip, country, region, city, isp string
}
}{
{
name: "Test IPIP Database",
ip: "1.1.1.1",
expected: struct {
ip, country, region, city, isp string
}{
ip: "1.1.1.1",
country: "澳大利亚",
region: "新南威尔士州",
city: "悉尼",
isp: "Cloudflare",
},
},
{
name: "Test CDN Database",
ip: "8.8.8.8",
expected: struct {
ip, country, region, city, isp string
}{
ip: "8.8.8.8",
country: "美国",
region: "加利福尼亚州",
city: "山景城",
isp: "Google",
},
},
{
name: "Test ToB Database",
ip: "114.114.114.114",
expected: struct {
ip, country, region, city, isp string
}{
ip: "114.114.114.114",
country: "中国",
region: "江苏省",
city: "南京市",
isp: "中国电信",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ip, country, region, city, isp := ParseIP(tt.ip)
if ip != tt.expected.ip || country != tt.expected.country ||
region != tt.expected.region || city != tt.expected.city ||
isp != tt.expected.isp {
t.Errorf("ParseIP() = %v, %v, %v, %v, %v, want %v, %v, %v, %v, %v",
ip, country, region, city, isp,
tt.expected.ip, tt.expected.country, tt.expected.region,
tt.expected.city, tt.expected.isp)
}
})
}
}
// 测试ParseIP函数
func TestParseIP(t *testing.T) {
tests := []struct {
name string
ip string
expected struct {
ip, country, region, city, isp string
}
}{
{
name: "Test Valid IP",
ip: "8.8.8.8",
expected: struct {
ip, country, region, city, isp string
}{
ip: "8.8.8.8",
country: "美国",
region: "加利福尼亚州",
city: "山景城",
isp: "Google",
},
},
{
name: "Test Invalid IP",
ip: "invalid.ip",
expected: struct {
ip, country, region, city, isp string
}{
ip: "invalid.ip",
country: "unknown",
region: "unknown",
city: "unknown",
isp: "unknown",
},
},
{
name: "Test Private IP",
ip: "192.168.1.1",
expected: struct {
ip, country, region, city, isp string
}{
ip: "192.168.1.1",
country: "unknown",
region: "unknown",
city: "unknown",
isp: "unknown",
},
},
{
name: "Test Special Region",
ip: "36.255.56.0",
expected: struct {
ip, country, region, city, isp string
}{
ip: "36.255.124.0",
country: "中国",
region: "香港",
city: "香港",
isp: "中国电信",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ip, country, region, city, isp := ParseIP(tt.ip)
if ip != tt.expected.ip || country != tt.expected.country ||
region != tt.expected.region || city != tt.expected.city ||
isp != tt.expected.isp {
t.Errorf("ParseIP() = %v, %v, %v, %v, %v, want %v, %v, %v, %v, %v",
ip, country, region, city, isp,
tt.expected.ip, tt.expected.country, tt.expected.region,
tt.expected.city, tt.expected.isp)
}
})
}
}
可以看到撰写了两个基本的测试用例,包含了几种特别的用例类型
不过,这里也出现了copilot的问题:
1.Ip解析的预期结果不对,比如36.255.124.0其实是新加坡的IP地址
2.TestIPSources的用例使用了ParseIP方法,而没有用原始的三种数据源解析
!24 当然,用例数据输入质量的问题,本身不是copilot的责任,构建优质的测试用例的几种类别就好了!
修正下:
第一个需要修改下,不同的IP源使用的函数式ipipdb.Search、ipdb.Search、tobdb.Search
package ip
import (
"net"
"testing"
)
// 测试不同IP源的Search函数
func TestIPSourceSearch(t *testing.T) {
tests := []struct {
name string
ip string
searchFn func(net.IP) *ipcity.Meta
expected struct {
country, region, city, isp string
}
}{
{
name: "Test IPIP Database Search",
ip: "1.1.1.1",
searchFn: ipipdb.Search,
expected: struct {
country, region, city, isp string
}{
country: "澳大利亚",
region: "新南威尔士州",
city: "悉尼",
isp: "Cloudflare",
},
},
{
name: "Test CDN Database Search",
ip: "8.8.8.8",
searchFn: ipdb.Search,
expected: struct {
country, region, city, isp string
}{
country: "美国",
region: "加利福尼亚州",
city: "山景城",
isp: "Google",
},
},
{
name: "Test ToB Database Search",
ip: "114.114.114.114",
searchFn: tobdb.Search,
expected: struct {
country, region, city, isp string
}{
country: "中国",
region: "江苏省",
city: "南京市",
isp: "中国电信",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ipAddr := net.ParseIP(tt.ip)
if ipAddr == nil {
t.Fatalf("Invalid IP address: %s", tt.ip)
}
meta := tt.searchFn(ipAddr)
if meta == nil {
t.Fatalf("Search returned nil for IP: %s", tt.ip)
}
if meta.Country() != tt.expected.country ||
meta.Province() != tt.expected.region ||
meta.City() != tt.expected.city ||
meta.ISP() != tt.expected.isp {
t.Errorf("Search() = %v, %v, %v, %v, want %v, %v, %v, %v",
meta.Country(), meta.Province(), meta.City(), meta.ISP(),
tt.expected.country, tt.expected.region, tt.expected.city, tt.expected.isp)
}
})
}
}
nice,使用了searchFn方法,这次生成的不错!
这里通过一个案例来进行了展示从需求文档到前期准备到代码撰写、调优、以及单元测试的过程,当然了当你读到这里的话,就会发现这篇文章很长
但其实笔者很懒,那笔者是怎么写出来这篇文章的呢?很简单:
我想总结下我们今天的对话,沉淀成一篇关于程序员与AI对话撰写一个业务逻辑的文章,帮我列个提纲
对,我在套娃
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。