部署DeepSeek模型,进群交流最in玩法!
立即加群
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >工程师之面向copilot编程

工程师之面向copilot编程

原创
作者头像
粲然忧生
发布2025-03-31 21:25:46
发布2025-03-31 21:25:46
1821
举报
文章被收录于专栏:工程师的分享工程师的分享

前言

笔者已经使用copilot协助开发有一段时间了,但一直没有总结一个协助代码开发的案例,特别是怎么问copilot,按照什么顺序问,哪些方面可以高效的生成需要的代码,这一次,笔者以IP解析需求为例,沉淀一个实践案例,供大家参考

当然,其实也不局限于copilot本身,类似的VsCode插件有很多,本文也只是拿chat的AI大模型做例子,只要是deepseek-v3就好

需求文档

为了聚焦,具体需求做了些抽象,简单的说,需要对系统一个IP解析功能进行更新:

1.以前使用了A库和B库进行解析,现在需要增加C库进行解析,

2.需要对三个库解析的结果进行优先级判断,确保把最优结果进行输出

前期准备

这一步很重要,因为很多时候当我们拿到需求文档的时候,希望直接给到IDE的AI助手,结果一般事与愿违,因为AI助手适合在一个限定范围内学习和给出高质量意见

所以要做一些简单前期工作——目的是让copilot学习尽量少的代码资料,从而减少幻觉的输出

1.写代码把C库加载进来

最好把三个库加载的代码先尽量写到一个文件中,比如:

代码语言:go
复制
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生成

2.找到解析函数代码

IP解析函数中,要包含对A、B库的调用及综合算法

因为涉及综合算法,最好把综合算法放到一个文件中,这样copilot就可以读更少的文件

然后把确定输入代码的地方,写个注释,表示要在这里写

比如:

代码语言:go
复制
// 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,这样剩下的变量名也可以自助生成

3.把相关的文件放到copilot中,选择deepseek- v3模型

这里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来说,逻辑十分的混乱

我们来分析下为什么混乱:

  1. 没有按照1、2、3标题,这让copilot难以分段
  2. 逻辑这里最终输出是if、while的类型,所以需要写清晰优先级
  3. “其中一个重要逻辑”,这种十分的口语,其实对于程序员也会难以理解
  4. 最后的特殊逻辑,太过于细节,可以在生成基础代码的基础上再进行“优化”

!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,帮我写下这个代码

这里对上述的问题进行了修正,同时增加几点优化:

  1. 而且明确了代码撰写在什么地方,更加限定了copilot的使用场景
  2. 描述逻辑时用了大量相似的语句,减少的copilot分析难度,即输入“教条”一些

我们看下结果:

代码语言:go
复制
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()
        }
    }

我们可以看到效果还是比较明显的:

  1. 通过清晰的表述,copilot成功识别到了三种库的获取方式,自动生成C库的获取和解析逻辑,
  2. 统计优先级的表示,copilot完全按照逻辑思路写了if-else语句,方便判断
  3. 在if语句中对表述进行了合并
  4. 生成了一个最终的赋值结构体,优化了代码

二、业务修改及对比测评

2.1 根据真实业务和可读性修改

通过刚才的表述,我们生成了一个“基础”的逻辑代码

这个时候,我们可能会发现一些问题,并对源码进行了部分修正

比如笔者修改了下逻辑:

代码语言:go
复制
// 处理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这样的命名对于研发同学来说还是不太友好

2.2 对比测评

修改完毕后,我们需要copilot帮我们对比下自动生成的代码和人工修改的的区别,同时也可以验证我们修改的逻辑是否“正确”

比如下面的这段话:

我修改好了,帮我看看我写的和你写的区别,并从代码质量、可读性、性能等角度分析下不同

copilot输出如下:

代码语言:txt
复制
从代码质量、可读性和性能等角度分析,你的代码和我的代码主要有以下区别:

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获取的优点,说明,从研发角度,前者通常意义上更“好”一些,这里我们选择采纳,同时可以提出了一个懒得核实的小疑问

2.3 按建议修改并让copilot干些“脏活累活”

“判空处理”一般来说是一个标准的“脏货累活”,因为需要花一段时间来走查代码确定,相似的还有“错误处理”,这里可以交给“copilot”来做

我按照你的建议修改了,但我有个疑问44行-46行,需要做判空处理吗

看看copilot的输出

代码语言:txt
复制
关于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不做判空的解释,省去了走查代码的时间

看看最后生成的代码:

代码语言:go
复制
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编程就是需要先把业务逻辑搞对再优化代码结构——这是两个不同的步骤,不能省了略!

3.1 输入优化的提示

这里可以把不顺眼的地方着重提出来

基于我的逻辑,那这里还有没有比较好的逻辑推荐,也可以推荐性能、可用性、可读性、健壮性上的优化

看看copilot的输出:

代码语言:txt
复制
基于你的代码逻辑,我从性能、可用性、可读性和健壮性方面提出以下优化建议:

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也给出了完整修改代码的样例:

代码语言:go
复制
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()
}

可以看到:

  1. copilot把优先级判断的逻辑进行了整合
  2. copilot把ISP特殊处理逻辑也进行了函数化
  3. 增加函数初始值的判断
  4. 并且增加了异常的panic
  5. 整体的逻辑清晰了很多

3.2 添加特殊逻辑

逻辑整体清晰了之后,再添加一些特殊逻辑进来这样的话,可以让代码始终处在可读可控的状态

比如:

增加一个特殊逻辑,如果识别出来的country包含香港、澳门字样,需要增加中国前缀,如果region里面包含香港、澳门字样,需要把country固定设置为中国

copilot输出如下:

代码语言:go
复制
// ==== 原代码 ====
// 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
// ...省略其他代码...

可以看到

最终的代码在:

代码语言:go
复制
// 处理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代码分析,再生成测试用例!

4.1 先分析代码

新开一个窗口避免之前的信息成为脏数据,同时也double check下代码逻辑的准确性

帮忙分析下ParseIP函数的逻辑

看看copilot的输出:

代码语言:txt
复制
我来分析一下`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的输出到正确

4.2 生成测试用例

如果觉得copilot的输出没有问题了就开始生成测试用例

我需要写两类测试用例,1.一类是针对不同的IP库源单独一个测试用例,输入是一个Ip列表,输出是IP地址、国家、地区、城市和ISP,2.一类是针对ParseIP的测试用例,撰写在这个文件夹所在地方,ip_test.go文件中

这里注意的点:

  1. 增加序号和类别,这里就明确表述出:两类,一类是,另一类是
  2. 明确单元测试的格式,即XX_test.go文件,熟悉golang开发的同学知道_test.go有明确的格式
代码语言: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)
			}
		})
	}
}

可以看到撰写了两个基本的测试用例,包含了几种特别的用例类型

  1. 国内国际的IP类别
  2. 有效无效的IP类别
  3. 公网局域网IP类别

不过,这里也出现了copilot的问题:

1.Ip解析的预期结果不对,比如36.255.124.0其实是新加坡的IP地址

2.TestIPSources的用例使用了ParseIP方法,而没有用原始的三种数据源解析

!24 当然,用例数据输入质量的问题,本身不是copilot的责任,构建优质的测试用例的几种类别就好了!

修正下:

第一个需要修改下,不同的IP源使用的函数式ipipdb.Search、ipdb.Search、tobdb.Search

代码语言:go
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 需求文档
    • 1.以前使用了A库和B库进行解析,现在需要增加C库进行解析,
    • 2.需要对三个库解析的结果进行优先级判断,确保把最优结果进行输出
  • 前期准备
    • 1.写代码把C库加载进来
    • 2.找到解析函数代码
    • 3.把相关的文件放到copilot中,选择deepseek- v3模型
  • 开始提效
    • 一、输入清晰的业务逻辑
    • 二、业务修改及对比测评
      • 2.1 根据真实业务和可读性修改
      • 2.2 对比测评
      • 2.3 按建议修改并让copilot干些“脏活累活”
    • 三、代码优化
      • 3.1 输入优化的提示
      • 3.2 添加特殊逻辑
    • 四、单元测试
      • 4.1 先分析代码
      • 4.2 生成测试用例
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档