​GitOps模式下微服务CI/CD实践

news2024/12/22 14:11:52

点击上方蓝字⭐️关注“DevOps云学堂”,接收最新技术实践

3a5accab4a03ef7221d1ea4c61f313bd.png

今天是「DevOps云学堂」与你共同进步的第 24 

3baef08d9adb8b1bf2b46d20f0c96bd4.jpeg

单应用与环境流程设计:

ea4b038bd5be5861abe6ad4f91760e40.png

多应用与环境流程设计:

6658e5b0e9c41386dcfced5f2ba98fb1.png


CI持续集成

首先准备一个代码库:https://github.com/DevOpsCICDCourse/microservicescicd/blob/main/microservice-demo-service-master.zip

b30cf69eaa0977aee7f664a93e28e9d8.png


我们来梳理一下CI流水线的步骤:

53d2c1d91da97013baba207dd124916c.png


  • 由于此次实现的代码仓库类型为单一存储库,即一个存储库存放多个服务模块代码,每个子目录为一个服务模块。

  • 首先,我们的持续集成流水线需要能够正确获取,当前的commit是哪个服务的代码。

  • 确定好服务,然后下载该服务的代码,进行编译打包、单元测试、代码扫描和构建镜像等步骤。

如何获取commit的服务信息?这里我们使用GitLab WebHook功能和Jenkins 的job 构建触发器对接来实现。

992d831cf1acff2ebcc386e23fd29dd2.png

工作流程是: 当我在Gitlab提交了代码,会通过GitLab webhook 触发Jenkins Scheduler 作业, 会将此次提交代码所产生的hook data数据信息以POST的方式传给Jenkins Job。 此时Jenkins job可以编写使用Generic Hook插件获取此次POST请求传输过来的请求体Body信息。 是一段JSON数据, 该job运行后编写Pipeline 解析JSON中的数据拿到所变更的服务模块信息。 最后触发对应服务的CI作业进行构建。

CI-Scheduler 作业

此作业只需要开启webhook, 配置触发token(唯一性)。生成hookurl:http://jenkins.idevops.site/generic-webhook-trigger/invoke?token=microservicecicd-scheduler-CI

bf8f2e4343e72505865c35ba6e53c9a2.png

d15d47d16e780e1e729f782335316dbd.png


Jenkinsfile

pipeline {
 agent any 

 stages{

  stage("GetData"){
   steps{
    script {
     echo "${webHookData}"

     data = readJSON  text: "${webHookData}"

     println(data)

     env.branchName = data.ref - "refs/heads/"
     env.commitId = data.checkout_sha
     env.projectId = data.project_id
     commits = data["commits"]

     println("${env.branchName}")
     println("${env.commitID}")
     println("${env.projectId}")

     //env.moduleName = "service01"
     changeServices = []
                    for(commit in commits) {
                        println(commit.id)

                        //added
                        for (add in commit.added) {
                            s = add.split("/") as List
                            if (s.size() > 1){
                                if (changeServices.indexOf(s[0]) == -1){
                                    changeServices.add(s[0])
                                }
                            }
                        }

                        //modified
                        for (m in commit.modified) {
                            s = m.split("/") as List
                            // println s
                            // println s.size()
                            // println s[0]
                            if (s.size() > 1){
                                // println changeServices.indexOf(s[0])
                                if (changeServices.indexOf(s[0]) == -1){
                                    changeServices.add(s[0])
                                }
                            }
                        }

                        //removed
                        for (r in commit.removed) {
                            s = r.split("/") as List
                            println s
                            if (s.size() > 1){
                                if (changeServices.indexOf(s[0]) == -1){
                                    changeServices.add(s[0])
                                }
                            }
                        }
                    }

                    println(changeServices)
                    //currentBuild.description = " Trigger by  ${eventType} ${changeServices} 
    }
   }
  }

  stage('DefineService') {
            steps {
                script{
                    println(changeServices)
                    //服务构建顺序控制
                    services = ['service02', 'service01']
                    for (service in services){
                        if (changeServices.indexOf(service) != -1){
                            jobName = 'microservicecicd-'+service+'-service-CI'
                            build job: jobName, wait: false,  parameters: [string(name: 'branchName', value: "${env.branchName}" ),
                                                                           string(name: 'commitId',   value: "${env.commitId}" ), 
                                                                           string(name: 'projectId',  value: "${env.projectId}" )]
                        }
                    }
                }
            }
        }
 }
}

GitLab 配置WebHook

开启webhook,配置hookurl:http://jenkins.idevops.site/generic-webhook-trigger/invoke?token=microservicecicd-scheduler-CI

482e9d3c68da5ee7ee3a12b808a9c115.png


CI流水线-CI作业

每个微服务创建一个CI作业,具有三个字符串参数:分支名称、commitID、项目ID。

45ed51db9e98876e84a394353554da62.png


Jenkinsfile

String branchName = "${env.branchName}"
String moduleName = "${JOB_NAME}".split("/")[1].split("-")[1]
String srcUrl = "http://gitlab.idevops.site/microservicecicd/microservicecicd-demo-service.git"
String commitId = "${env.commitId}"
String projectId = "${env.projectId}"

pipeline {
    agent { node { label "build" } }

    stages {
        stage('GetCode') {
            steps {
                script {
                    checkout([$class: 'GitSCM', 
                            branches: [[name: "${branchName}"]], 
                            doGenerateSubmoduleConfigurations: false,
                            extensions: [[$class: 'SparseCheckoutPaths', 
                                        sparseCheckoutPaths: [[path: "${moduleName}"],[path: 'Dockerfile']]]], 
                            submoduleCfg: [], 
                            userRemoteConfigs: [[credentialsId: 'gitlab-admin-user',
                                                url: "${srcUrl}"]]])
                }
                
            }
        }

        stage("Build&Test"){
            steps{
                script{
                    echo "Build..........."

                    sh """
                    cd ${moduleName} 
                    mvn clean package

                    """
                }
            }
            post {
                always {
                    junit "${moduleName}/target/surefire-reports/*.xml"
                }
            }
        }

        stage("SonarScan"){
            steps{
                script{

                    def sonarDate = sh returnStdout: true, script: 'date  +%Y%m%d%H%M%S'
                    sonarDate = sonarDate - "\n"

                    withCredentials([string(credentialsId: 'sonar-admin-user', variable: 'sonartoken'),
                                    string(credentialsId: 'gitlab-user-token', variable: 'gitlabtoken')]) {
                        // some block
                        sh """
                        cd ${moduleName} 
                        sonar-scanner \
                        -Dsonar.projectKey=${JOB_NAME} \
                        -Dsonar.projectName=${JOB_NAME} \
                        -Dsonar.projectVersion=${sonarDate} \
                        -Dsonar.ws.timeout=30 \
                        -Dsonar.projectDescription="xxxxxxx" \
                        -Dsonar.links.homepage=http://www.baidu.com \
                        -Dsonar.sources=src \
                        -Dsonar.sourceEncoding=UTF-8 \
                        -Dsonar.java.binaries=target/classes \
                        -Dsonar.java.test.binaries=target/test-classes \
                        -Dsonar.java.surefire.report=target/surefire-reports \
                        -Dsonar.host.url="http://sonar.idevops.site" \
                        -Dsonar.login=${sonartoken} \
                        -Dsonar.gitlab.commit_sha=${commitId} \
                        -Dsonar.gitlab.ref_name=${branchName} \
                        -Dsonar.gitlab.project_id=${projectId} \
                        -Dsonar.dynamicAnalysis=reuseReports \
                        -Dsonar.gitlab.failure_notification_mode=commit-status \
                        -Dsonar.gitlab.url=http://gitlab.idevops.site \
                        -Dsonar.gitlab.user_token=${gitlabtoken} \
                        -Dsonar.gitlab.api_version=v4

                        """

                    }
 
                }
            }
        }

        stage("BuildImage"){
            steps{
                script{

                     withCredentials([usernamePassword(credentialsId: 'aliyun-registry-admin', passwordVariable: 'password', usernameVariable: 'username')]) {
                
                         env.nowDate = sh  returnStdout: true, script: 'date  +%Y%m%d%H%M%S'
                         env.nowDate = env.nowDate - "\n"

                         env.releaseVersion = "${env.branchName}"
                         env.imageTag = "${releaseVersion}-${nowDate}-${commitId}"
                         env.dockerImage = "registry.cn-beijing.aliyuncs.com/microservicecicd/microservicecicd-${moduleName}-service:${env.imageTag}"
                         env.jarName = "${moduleName}-${branchName}-${commitId}"
                         sh """
                             docker login -u ${username} -p ${password}  registry.cn-beijing.aliyuncs.com
                             cd ${moduleName} && docker build -t ${dockerImage} -f ../Dockerfile --build-arg SERVICE_NAME=${jarName} .
                             sleep 1
                             docker push ${dockerImage}
                             sleep 1
                             docker rmi ${dockerImage}
                          """
                    }


                }
            }
        }

        
    }
}

GitOps-CI扩展部分

在原始CI作业的步骤基础上,增加了一个更新环境的步骤。GitOps实践会将当前的基础环境部署文件存放到一个Git仓库中。我们的CI作业在完成镜像上传后,同时更新环境部署文件中的镜像标签信息。(所以我们需要先获取该环境文件并更新上传)

40f3986097559d48d3b9c56ee149bddc.png


stage("PushFile"){
          // when {
          //   expression { "${env.branchName}".contains("RELEASE-") }
          // }
          steps{
            script{
              if ("${env.branchName}".contains("RELEASE-")){
                println("branchName = branchName")
                env.branchName = "master"

              } else {
                env.branchName = "feature"
              }

                for (i = 0; i < 3; i++) {
                    //下载版本库文件 
                    response = GetRepoFile(40,"${moduleName}%2fvalues.yaml", "${env.branchName}")
                    //println(response)
                    
                    //替换文件中内容
                    yamlData = readYaml text: """${response}"""

                    println(yamlData.image.version)
                    println(yamlData.image.commit)
                    yamlData.image.version = "${releaseVersion}-${env.nowDate}"
                    yamlData.image.commit  = "${commitId}"

                    println(yamlData.toString())

                    sh "rm -fr test.yaml"
                    writeYaml charset: 'UTF-8', data: yamlData, file: 'test.yaml'
                    newYaml = sh returnStdout: true, script: 'cat test.yaml'
                    
                    println(newYaml)
                    //更新gitlab文件内容
                    base64Content = newYaml.bytes.encodeBase64().toString()

                    // 会有并行问题,同时更新报错
                    try {
                      UpdateRepoFile(40,"${moduleName}%2fvalues.yaml",base64Content, "${env.branchName}")
                      break;
                    } catch(e){
                      sh "sleep 2"
                      continue;
                    }
                }
            }
          }
        }
        
 //封装HTTP请求
def HttpReq(reqType,reqUrl,reqBody){
    def gitServer = "http://gitlab.idevops.site/api/v4"
    withCredentials([string(credentialsId: 'gitlab-token', variable: 'gitlabToken')]) {
      result = httpRequest customHeaders: [[maskValue: true, name: 'PRIVATE-TOKEN', value: "${gitlabToken}"]], 
                httpMode: reqType, 
                contentType: "APPLICATION_JSON",
                consoleLogResponseBody: true,
                ignoreSslErrors: true, 
                requestBody: reqBody,
                url: "${gitServer}/${reqUrl}"
                //quiet: true
    }
    return result
}


//获取文件内容
def GetRepoFile(projectId,filePath,branchName){
    apiUrl = "projects/${projectId}/repository/files/${filePath}/raw?ref=${branchName}"
    response = HttpReq('GET',apiUrl,'')
    return response.content
}

//更新文件内容
def UpdateRepoFile(projectId,filePath,fileContent, branchName){
    apiUrl = "projects/${projectId}/repository/files/${filePath}"
    reqBody = """{"branch": "${branchName}","encoding":"base64", "content": "${fileContent}", "commit_message": "update a new file"}"""
    response = HttpReq('PUT',apiUrl,reqBody)
    println(response)

}
images

3ab6a394d751c9e37abb4bfa33de315a.png

GitOps-CD部分


fc2a69c633cd73fc9bc2e6e23111585e.png

CD-Scheduler作业

此作业其实也是接收GitLab的webhook请求, 与CI-scheduler作业类似。不同的是这个CD-scheduler作业是用来接收环境仓库的代码变更。开启webhook, 配置触发token。生成hookurl:http://jenkins.idevops.site/generic-webhook-trigger/invoke?token=microservicecicd-scheduler-CD

3ced52f06d03350d442ae7dc4ac879f6.png

95cf9fd2235d8d03c4f4e72d19039e76.png



Jenkinsfile

pipeline {
    agent any

    stages {
        stage('GetCommitService') {
            steps {
                script{
                    echo 'Hello World'
                    echo "${WebHookData}"
                    
                    // Git Info
                    webhookdata = readJSON text: """${WebHookData}"""
                    eventType = webhookdata["object_kind"]
                    commits = webhookdata["commits"]
                    branchName = webhookdata["ref"] - "refs/heads/"
                    projectID = webhookdata["project_id"]
                    commitID = webhookdata["checkout_sha"]


                    changeServices = []
                    for(commit in commits) {
                        println(commit.id)

                        //added
                        for (add in commit.added) {
                            s = add.split("/") as List
                            if (s.size() > 1){
                                if (changeServices.indexOf(s[0]) == -1){
                                    changeServices.add(s[0])
                                }
                            }
                        }

                        //modified
                        for (m in commit.modified) {
                            s = m.split("/") as List
                            // println s
                            // println s.size()
                            // println s[0]
                            if (s.size() > 1){
                                // println changeServices.indexOf(s[0])
                                if (changeServices.indexOf(s[0]) == -1){
                                    changeServices.add(s[0])
                                }
                            }
                        }

                        //removed
                        for (r in commit.removed) {
                            s = r.split("/") as List
                            println s
                            if (s.size() > 1){
                                if (changeServices.indexOf(s[0]) == -1){
                                    changeServices.add(s[0])
                                }
                            }
                        }
                    }

                    println(changeServices)
                    currentBuild.description = " Trigger by  ${eventType} ${changeServices} "
                }
            }
        }

        stage('DefineService') {
            steps {
                script{
                    println(changeServices)
                    //服务构建顺序控制
                    services = ['service02', 'service01']
                    for (service in services){
                        if (changeServices.indexOf(service) != -1){
                            jobName = 'microservicecicd-'+service+'-service-CD'
                            build job: jobName, wait: false,  parameters: [string(name: 'branchName', value: "${branchName}" )]
                        }
                    }
                }
            }
        }
    }
}

环境库配置webhook

开启webhook,配置hookurl:http://jenkins.idevops.site/generic-webhook-trigger/invoke?token=microservicecicd-scheduler-CD

4852172119cb7fd9534833c427df8283.png


CD流水线-CD作业

36de75db43eea426a820f42314fbf633.png


Jenkinsfile

String serviceName ="${JOB_NAME}".split("-")[1]
String nameSpace = "${JOB_NAME}".split("-")[0].split("/")[-1]


//pipeline
pipeline{
    agent { node { label "k8s"}}
    
    stages{

       stage("GetCode"){
            steps{
                script{
                    println("${branchName}")
                    println("${env.branchName}".contains("RELEASE-"))
                    println "获取代码"
                    checkout([$class: 'GitSCM', branches: [[name: "${env.branchName}"]], 
                                      doGenerateSubmoduleConfigurations: false, 
                                      extensions: [[$class: 'SparseCheckoutPaths', 
                                                    sparseCheckoutPaths: [[path: "${serviceName}"]]]], 
                                      submoduleCfg: [], 
                                      userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "http://gitlab.idevops.site/microservicecicd/microservicecicd-env.git"]]])
                }
            }
        }

        stage("HelmDeploy"){
            steps{
                script{
                  sh """
                      kubectl create ns "${nameSpace}-uat"  || echo false

                      helm install "${serviceName}" --namespace "${nameSpace}-uat" ./"${serviceName}" ||  helm upgrade "${serviceName}" --namespace "${nameSpace}-uat" ./"${serviceName}"

                      helm list --namespace "${nameSpace}-uat"
                      helm history "${serviceName}" --namespace "${nameSpace}-uat"

                  """
                }
            }
        }
    }
}

dbb1f1ce3f993d4935661aaf1d929bc5.png


往期推荐

为什么CI/CD 管道需要DevOps质量关卡?

代码发布后发现漏洞? | DevOps工程类D001 (术语解析与扩展)

《 Istio 权威指南 》重磅发行!华为云云原生团队匠心著作

站点可靠性工程与 DevOps 有何不同

如果这篇文章对您有帮助,欢迎转发点赞分享。您的关注是我持续分享的动力!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/531218.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Angular之创建项目报错:setTimeout is not defined

零基础的宝们&#xff0c;跟着视频学习Angular中&#xff0c;会教授大家如何创建一个新项目。 但是在操作时就会遇到无法创建的问题。 接下来我们一起来看看&#xff0c;本人Angular起步时卡在家门口的问题。 在已经安装了nodejs的情况下&#xff0c;被建议使用cnpm命令全局安装…

MySQL 8 如何解决快速获取数据库中所有业务库表列的distinct 值,不使用SQL

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

推荐 5 个本周 火火火 的开源项目

本周推荐开源项目目录&#xff1a; 1. 你的私人 GPT 2. 集成 AI 的数据库客户端工具 3. Switch 模拟器 4. 开源的短视频生成和编辑工具 5. 医疗领域的微调模型 01 你的私人 GPT privateGPT 开源两周&#xff0c;便斩获了 10K 的 Star。可以在离线的情况下&#xff0c;使用 GPT …

AMBER分子动力学模拟之分子模拟-- HIV蛋白酶-抑制剂复合物(2)

AMBER分子动力学模拟之分子模拟-- HIV蛋白酶-抑制剂复合物(2) 体系平衡 体系的优化分为两步&#xff0c;第一步固定蛋白和小分子&#xff0c;对添加的水盒优化。第二步则是对整个体系的优化。 对水盒子优化 vim min1.in &cntrl imin1, maxcyc10000, ncyc5000, ntb1, …

杂记——22.强弱类型语言的区别

这篇文章我们来讲一下强类型语言和弱类型语言&#xff0c;并分析一下二者的区别 目录 1.强类型语言 2.弱类型语言 3.区别 3.1编译型和解释型 3.2动态语言和静态语言 3.3 强类型定义语言和弱类型定义语言 4.静态类型语言的优势 1.强类型语言 强类型语言也称为强类型定义…

error C2143: 语法错误 : 缺少“)”(在“常量”的前面)

【背景】最近做一个项目&#xff0c;需要使用到凌华的运动控制卡和IO卡&#xff0c;在测试工程中&#xff0c;使用的类和引用的库文件都一切正常。 测试完成后&#xff0c;本着可重复移植的考虑&#xff0c;将整个文件夹添加到现有工程中&#xff0c;却出现了非常奇怪的编译错误…

JVM系列-第9章-StringTable(字符串常量池)

StringTable&#xff08;字符串常量池&#xff09; String的基本特性 String&#xff1a;字符串&#xff0c;使用一对 “” 引起来表示 String s1 "atguigu" ; // 字面量的定义方式String s2 new String("hello"); // new 对象的方式String被…

基于html+css的图展示73

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

【fly-iot飞凡物联】():开源项目Apache IoTDB,开源项目学习,原来还有这样的项目,关于IOT的几个开源项目汇总下

目录 前言1&#xff0c;关于&#xff1a;开源项目Apache IoTDB2&#xff0c;还有个admin后台3&#xff0c;thinglinks项目4&#xff0c;thingsboard-ui-vue项目5&#xff0c;apache pulsar项目6&#xff0c;ActorCloud项目 前言 本文的原文连接是: https://blog.csdn.net/freew…

ASP.NET MVC-WebAPI请求

一、无参数Get请求 一般的get请求我们可以使用jquery提供的$.get() 或者$.ajax({type:"get"}) 来实现&#xff1a; 请求的后台Action方法仍为上篇文章中的GetUser() : 也可以用$.ajax({type:"get"}) 方式&#xff0c;正确的获得了返回数据&#xff1a; 二、…

Grafana功能菜单介绍(04)

Grafana的功能菜单设计为侧边栏(sidebar)形式,可以折叠隐藏,便于我们更加专注数据的可视化。现将菜单栏各项功能进行编号讲解,如下图所示。 ① Grafana的logo,即当前页为Grafana的Home page,在任何页面点击Grafana的logo,都会跳到Home Page。 ② 新建按钮,用于创建Da…

Camtasia Studio2023新功能及下载安装使用教程

Camtasia Studio2023新版本包含了屏幕录像、视频剪辑和编辑、视频录音配音、视频菜单制作、视频剧场和视频播放等功能。它可以在任何颜色模式下记录屏幕动作&#xff0c;包括影像、 音效、鼠标移动轨迹&#xff0c;解说声音等等&#xff1b;它支持视频播放和编辑压缩&#xff0…

Mars3d实现单击按钮高亮矢量对象

调用高亮的关键代码&#xff1a; let graphictruck new mars3d.graphic.ModelEntity({ position: [116.327881, 31.018378, 5000], style: { url: //data.mars3d.cn/gltf/mars/qiche.gltf, heading: 90, scale: 0.9, …

应用软件系统架构设计的“七种武器”

对于软件架构这一概念&#xff0c;有太多的版本&#xff0c;目前在业界由大师级人物或组织提出的对这一概念的阐述就超过十种以上&#xff0c;我个人比较赞同RUP(Rational Unified Process)中对软件架构的定义&#xff0c;即软件架构包含了关于以下问题的重要决策&#xff1a; …

剑指offer(C++)-JZ48:最长不含重复字符的子字符串(算法-动态规划)

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 题目描述&#xff1a; 请从字符串中找出一个最长的不包含重复字符的子字符串&#xff0c;计算该最长子字符串的长度。 数据范围…

c++ 11标准模板(STL) std::set(四)

定义于头文件 <set> template< class Key, class Compare std::less<Key>, class Allocator std::allocator<Key> > class set;(1)namespace pmr { template <class Key, class Compare std::less<Key>> using se…

让 ChatGPT 来担任这个召之即来挥之即去的私人健身教练

健身教练 和ChatGPT聊天可以运用在各种正式、非正式&#xff0c;工作、休闲场合&#xff0c;让我们再来看一个场景&#xff0c;也是年轻人群体中非常热门的话题&#xff1a;健身。健身已经是年轻人最流行的活动&#xff0c;既可以是私密的个人健身&#xff0c;也可以是呼朋唤友…

JAVA集合,复杂度,泛型

泛型: 1.存储数据的时候 可以帮我们进行自动的类型检查 2.获取元素的时候&#xff0c;可以帮我们进行类型转换 获取数据时&#xff0c;不需要强转类型转换。 类型形参一般使用一个大写字母表示&#xff0c;常用的名称有&#xff1a; E 表示 Element K 表示 Key V 表示 …

多目标跟踪:视觉联合检测和跟踪

国内头部以自动驾驶全站技术为主线的交流学习社区&#xff08;感知、归控等&#xff09;&#xff0c;包含大量前沿论文解读、工程实践(源代码)、视频课程&#xff0c;热招岗位。欢迎加入&#xff01; 点击上方“迈微AI研习社”&#xff0c;选择“星标★”公众号 重磅干货&#…

Python每日一练(20230516) 打家劫舍 I\II\III\IV HouseRobber

目录 1. 打家劫舍 I House Robber i 2. 打家劫舍 II House Robber ii 3. 打家劫舍 III House Robber iii 4. 打家劫舍 IV House Robber iv &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏…