GO语言上手-基础语言 | 青训营笔记

238 阅读12分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

思维导图

GO语言上手-基础语言.png

图片格式

GO语言上手-基础语言1.png

文本格式

  • Go基础
    Hello, World · Go语言圣经 (studygolang.com)
    Go 语言教程 | 菜鸟教程 (runoob.com)

    • 入门\

      • Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令(译注:静态编译)。\
      • Go语言提供的工具都通过一个单独的命令go调用,go命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件。(本书使用$表示命令行提示符。)\
      • $ go build helloworld.go\

        • 这个命令生成一个名为helloworld的可执行的二进制文件(译注:Windows系统下生成的可执行文件是helloworld.exe,增加了.exe后缀名),之后你可以随时运行它(译注:在Windows系统下在命令行直接输入helloworld.exe命令运行),不需任何处理(译注:因为静态编译,所以不用担心在系统库更新的时候冲突,幸福感满满)\
      • Go语言的代码通过包(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组成,目录定义包的作用。\

        • main包比较特殊。它定义了一个独立可执行的程序,而不是一个库。\
        • 在main里的main 函数 也很特殊,它是整个程序执行时的入口(译注:C系语言差不多都这样)。\
      • 组成程序的函数、变量、常量、类型的声明语句(分别由关键字func、var、const、type定义)\
      • Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响Go代码的正确解析\

        • 比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字break、continue、fallthrough或return中的一个、运算符和分隔符++、--、)、]或}中的一个\
        • 举个例子,函数的左括号{必须和func函数声明在同一行上,且位于末尾,不能独占一行,而在表达式x + y中,可在+后换行,不能在+前换行\

          • 以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被分号分隔符,从而导致编译错误\
      • Go语言在代码格式上采取了很强硬的态度。gofmt工具把代码格式化为标准格式\

        • 很多文本编辑器都可以配置为保存文件时自动执行gofmt\
    • 命令行参数\

      • os包以跨平台的方式,提供了一些与操作系统交互的函数和变量。\

        • 程序的命令行参数可从os包的Args变量获取;os包外部使用os.Args访问该变量。\
        • os.Args变量是一个字符串(string)的切片(slice)\
        • os.Args的第一个元素:os.Args[0],是命令本身的名字;其它的元素则是程序启动时传给它的参数。\
      • 变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的零值(zero value),数值类型是0,字符串类型是空字符串""\
      • :=是短变量声明(short variable declaration)的一部分,这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句\
      • i--给i减1。它们是语句,而不像C系的其它语言那样是表达式。所以j = i++非法,而且++和--都只能放在变量名后面,因此--i也非法。\
      • 每次循环迭代,range产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但range的语法要求,要处理元素,必须处理索引。\

        • 一种思路是把索引赋值给一个临时变量(如temp)然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。\
        • Go语言中这种情况的解决方法是用空标识符(blank identifier),即_(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。大多数的Go程序员都会像上面这样使用range和_写echo程序,因为隐式地而非显式地索引os.Args,容易写对。\
      • 声明一个变量有好几种方式\

        • 第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。\
        • 第二种形式依赖于字符串的默认初始化零值机制,被初始化为""。\
        • 第三种形式用得很少,除非同时声明多个变量。\
        • 第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。\
        • 实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。\
      • 每次循环迭代字符串s的内容都会更新。+=连接原字符串、空格和下个参数,产生新字符串,并把它赋值给s。s原来的内容已经不再使用,将在适当时机对它进行垃圾回收。如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用strings包的Join函数\
  • 猜谜游戏\

    • 使用rand.Intn(maxNum)之前要设置随机数种子,不然每次运行都是相同值\

      • 用启动的时间戳初始化随机数种子\
    • 打开标准读入,并读入一行\

      • 通过引入"os"包,使用os.Stdin\
      • 将os.Stdin文件转化为流\

        • reader := bufio.NewReader(os.Stdin)\
      • 再利用reader.ReadString('\n')从流中读取\

        • 返回值会包含结束字符,需要利用input = strings.TrimSuffix(input,"\r\n")去除返回的值\

          • windows中要回车符+换行符 ("\r\n")才会回车+换行\
          • 缺少一个控制符或者顺序不对都不能正确的另起一行\
    • strconv.Atoi将字符串转化成数字\

      • guess, err := strconv.Atoi(input)\
  • 在线词典介绍\

    • JSON序列化\

      • 先建立JSON 结构体\
      • 先初始化一个结构体变量\

        • request := DictRequest{TransType: "en2zh", Source: "good"}\
      • 将其序列化为bytes数组\

        • buf, err := json.Marshal(request)\

          • 返回bytes数组\
        • var data = bytes.NewReader(buf)\
    • JSON反序列化\

      • 写一个和Response结构一一对应的结构体,并反序列化\
      • 保证是正确的返回报文\

        • 检测状态码是否为200,出错反序列化的为空结构\
      • 反序列化并打印字段\

        • err = json.Unmarshal(bodyText, &dictResponse)\
        • fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)\
  • SOCKS5 代理\

    • 原理\

      • 正常浏览器访问一个网站,如果不经过代理服务器的话,就是先和对方的网站建立TCP连接,然后三次握手,握手完之后发起HTTP请求,然后服务返回HTTP响应。\
      • 如果设置代理服务器之后首先是浏览器和socks5代理建立TCP连接,代理再和真正的服务器建立TCP连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、relay阶段。\

        • 第一个握手阶段\

          • 浏览器会向sock5s代理发送请求\

            • 包的内容包括一个协议的版本号,还有支持的认证的种类\
          • socks5服务器会选中一个认证方式,返回给浏览器。\

            • 如果返回的是00的话就代表不需要认证,返回其他类型的话会开始认证流程\
        • 第二个阶段是认证阶段\
        • 第三个阶段是请求阶段\

          • 认证通过之后浏览器会socks5服务器发起请求\

            • 主要信息包括版本号,请求的类型\
            • 一般主要是comection请求,就代表代理服务器要和某个域名或者某个P地址某个端口建立TCP连接。\
          • 代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。\
        • 第四个价阶段是relay阶段\

          • 此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。\
          • 然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边\
          • 然后实际上代理服务器并不关心流量的细节,可以是HTTP流量,也可以是其它TCP流量\
    • TCP echo server
      erver的工作逻辑很简单,你给他发送啥,他就回复啥

      • main函数\

        • 先用netlisten去监听一个端口,会返回一个sever\

          • server, err := net.Listen("tcp", "127.0.0.1:1080")\
        • 然后在一个死循环里面,每次去accept一个请求,成功就会返回一个连接\

          • client, err := server.Accept()\
        • 接下来的话我们在一个process函数里面去处理这个连接\

          • go process(client)\
          • go关键字,代表启动一个goroutiue\
          • 可以暂时类比为其他语言里面的启动一个子线程。只是这里的goroutinue 的开销会比子线程要小很多,可以很轻松地处理上万的并发。\
      • process 函数的实现\

        • 第一步defer connection.Close()\

          • defer的含义就是代表在这个函数退出的时候要把这个连接关掉,否则会有资源的泄露\
        • 接下来创建一个带缓冲的只读流\

          • reader := bufio.NewReader(conn)\
        • 调用ReadByte 函数来读取单个字节\

          • b, err := reader.ReadByte()\
        • 再把这一个字节写进去连接\

          • _, err = conn.Write([]byte{b})\
    • 认证阶段\

      • 实现一个空的auth 函数,在process函数里面调用,再来编写auth 函数的代码。\
      • 认证阶段的逻辑\

        • 第一步,浏览器会给代理服务器发送一个包,然后这个包有三个字段\

          • 第一个字段,version也就是协议版本号,固定是5\
          • 第二个字段methods,认证的方法数目\
          • 第三个字段每个method的编码,0代表不需要认证,2代表用户名密码认证\
      • auth函数\

        • 先用ReadByte来把版本号读出来\
        • 再读取methodSize,也是一个字节\
        • make一个相应长度的一个slice ,用io.ReadFull把它去填充进去\
        • 代理服务器还需要返回一个response,返回包包括两个字段,一个是version一个是method,,也就是选中的鉴传方式\

          • 实现不需要鉴传的方式,也就是00\
    • 实现请求阶段\

      • 读取携带URL或者IP地址+端口的包,然后把它打印出来。实现一个和auth函数类似的connect函数,同样在process里面去调用\
      • 请求阶段的逻辑\

        • 浏览器会发送一个包含如下6个字段的包,需要逐个去读取这些字段\

          • version版本号,还是5\
          • command,代表请求的类型\
          • RSV保留字段,不理会。\
          • atype就是目标地址类型,可能是IPV4 IPV6或者域名\
          • addr,这个地址的长度是根据 atype的类型而不同的\
          • port端口号,两个字节\
      • connect函数\

        • 这四个字段总共四个字节,定义一个长度为4的缓冲区,然后使用ReadFull一次性读出把它渎满\
        • 读满之后,然后第0个、第1个、第3个、分别是version cmd和typeversion\
        • 需要判断是socket 5, cmd需要判断是1\
        • 下面的atype,可能是 iv4 , ipv6,或者是host\

          • 如果IPV4的话,我们再次读满这个buf,因为这个buf长度刚好也是4个字节,然后逐个字节打印成IP地址的格式保存到addr变量\
          • 如果是个host的话,需要先读它的长度,再make一个相应长度的切片buf填充它。再转换成字符串保存到addr\
        • 最后还有两个字节是port,读取,然后按协议规定的大端字节序转换成数字\
        • 需要返回一个包,这个包有很多字段,但其实大部分都不会使用\

          • 第一个是版本号:socket 5\
          • 第二个,就是返回的类型,这里是成功就返回0\
          • 第三个是保留字段填0\
          • 第四个atype地址类型填1\
          • 第五个,第六个暂时用不到,都填成0。\
          • 一共4+4+2个字节,后面6个字节都是0填充\
    • relay阶段\

      • 我们直接用net.Dial建立一个TCP连接\
      • 建立完连接之后,加一个defer来关闭连接\
      • 建立浏览器和下游服务器的双向数据转发\

        • 标准库的io.copy可以实现一个单向数据转发,双向转发的话,需要启动两个goroutinue\

          • 两个Copy方向相反\
        • connect 函数执行完会立刻返回,返回的时候连接就被关闭了,需要等待任意一个方向copy出错的时候,再返回connect函数\

          • 这里可以使用到标准库里面的一个context 机制,用context连 with cancel 来创建一个context.\
          • 在最后等待ctx.Done(),只要cancel被调用,ctx.Done就会立刻返回。然后在上面的两个goroutinue里面调用一次cancel即可\
    • 测试\

      • 在浏览器里面测试代理需要安装switchomega插件\
      • 然后里面新建一个情景模式,代理服务器选socks5,代理服务器 127.0.0.1,端口1080,应用选项,保存,再次点击扩展\
      • 所有流量都将经过此代理服务器\
  • 作业\

    • 修改第一个例子猜谜游戏里面的最终代码,使用 fmt.Scanf 来简化代码实现\

      • go 强类型语言使用变量之前应该先定义\
      • Windows使用fmt.Scanf也要添加\r\n\
    • 修改第二个例子命令行词典里面的最终代码,增加另一种翻译引擎的支持\
    • 在上一步骤的基础上,修改代码实现并行请求两个翻译引擎来提高响应速度\