五行解决反转链表问题 以及其引伸

题目如下

  1. 反转链表

反转一个单链表。
示例:
  输入: 1->2->3->4->5->NULL
  输出: 5->4->3->2->1->NULL
进阶:
  你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

力扣206题,传送门

解法:

func reverseList(head *ListNode) *ListNode {
    cur := head        //当前节点
    var prev *ListNode //前一个节点(”第一个“前一个节点为nil)
    for cur != nil {
        cur.Next, prev, cur = prev, cur, cur.Next
    }
    return prev
}

没错,就是这么简单XD。

解法详解:

2、3行,定义变量就不用说了。
4行,当当前节点不为空的时候,进行循环。

重点 第5行:该行完成了翻转、指针移动的这两个关键步骤。

使用了连等式(多值赋值)。在这一行,看样子像是给三个变量进行更新赋值,实则它还隐藏着三个临时变量。
这里就需要先了解一下 多值赋值 的运算过程了:
  多值赋值会在当前变量所拥有的值的基础上,先计算等号右侧表达式的各个值,并在必要的情况下存储在临时变量中。再将右侧表达式的各个值,依次赋给等号左侧的变量(顺序为从左到右)。

详细分析多变量的赋值,请看这篇文章→《深入探讨Go多变量赋值》

  1. 先计算(备份)变量的值 到 临时变量中
    即:先备份当前的 prev, cur,记作 Temp_prev, Temp_cur
    计算当前的 cur.Next,存入 Temp_cur_Next中。
  2. 再对节点进行翻倒(赋值)
    cur.Next = Temp_prev  //将 当前节点的后继指针 指向 前一个节点。(完成翻转)
    prev = Temp_cur    //将 前一个节点 定义为 当前节点。(完成前一个节点指针的移动)
    cur = Temp_cur_Next  //将 当前节点 定义为 旧的“当前节点”的下一个节点。(完成当前节点指针的移动)

次重点 第7行:返回的结果为什么是prev(前一个节点),而不是cur(当前节点)?

在 最后一个节点 进行计算前,链表是这样的,1 <- 2 <- 3 <- 4 5 -> nil

  1. 计算后,赋值前的情况(计算了等号右侧的变量值,并存储在了临时变量里,还没有给等号左侧的变量修改值的时候):
    此时:prev = 4; cur = 5; Temp_prev = 4; Temp_cur = 5; Temp_cur_Next = nil;

  2. 计算后,开始对等号左侧的变量进行赋值(开始修改等号左侧变量的时候):
    备注:括号中的值代表的指针当前指向的元素
    cur.Next(5.Next) = prev(4)  //完成了最后两个元素的翻转
    prev = Temp_cur(5)     //即:翻转后得到的新链表的 head
    cur = Temp_cur_Next(nil)  //达到 for解除循环的临界条件:cur = nil

显然,cur = nilcur不能作为结果返回给被调用的函数


作者能力有限,若有哪些地方有误,请在评论区里指正。愿我们一同进步!

深入探讨 Go 多变量赋值

使用go语言时,会经常把一些变量放在同一行来声明、赋值或计算。那么,这种赋值方法到底是以什么样的实现方法来赋值的呢?

先说结论,就两步:
一、先计算等号右侧所有表达式的值,将结果存储临时变量中。
二、将临时变量的值赋给等式左侧的变量。

多个变量一同声明并赋值

a, b := 1, 5        //情况一
c, d := a+b, a-b    //情况二

对于情况一来说,很简单,直接按照从左到右的顺序,把1、2两个值赋值给a、b

  ; -----情况一  相关汇编代码  开始----------
  (.\main.go:4)     MOVQ    1, "".a+24(SP)             ; 将1赋值给a
  (.\main.go:4)     MOVQ5, "".b+16(SP)           ; 将5赋值给b
  ; -----情况一  相关汇编代码  结束----------

对于情况二来说,赋值的过程是这样的:
1. 计算a+b、a-b的值,并存储在两个临时变量中
2. 将运算结果从临时变量中调出来,从左到右依次赋给c, d两个变量。

  ; -----情况二  相关汇编代码  开始----------
  (.\main.go:5)     MOVQ    "".a+24(SP), AX           ; 将a赋值给AX寄存器
  (.\main.go:5)     ADDQ    $5, AX                    ; 将5和AX寄存器里的值相加(结果还保存在AX寄存器里)
  (.\main.go:5)     MOVQ    AX, ""..autotmp_4+56(SP)  ; 将AX寄存器里的值赋值给autotmp_4变量
  (.\main.go:5)     MOVQ    "".a+24(SP), AX           ; 将a赋值给AX寄存器
  (.\main.go:5)     SUBQ    "".b+16(SP), AX           ; 用AX里的值,减去b的值
  (.\main.go:5)     MOVQ    AX, ""..autotmp_5+48(SP)  ; 将AX里的计算结果赋值给autotmp_5变量
  (.\main.go:5)     MOVQ    ""..autotmp_4+56(SP), AX  ; 将autotmp_4变量放入AX寄存器中
  (.\main.go:5)     MOVQ    AX, "".c+8(SP)            ; 将AX寄存器中的值赋给c
  (.\main.go:5)     MOVQ    ""..autotmp_5+48(SP), AX  ; 将autotmp_5变量放入AX寄存器中
  (.\main.go:5)     MOVQ    AX, "".d(SP)              ; 将AX寄存器中的值赋给d
  ; -----情况二  相关汇编代码  结束----------

多个变量一同计算、互换变量值

func main() {
    a, b := 1, 5            //定义a, b
    c, d := a+b, a-b        //定义c ,d = 6, -4
    b, c, d = c, b, b+c     //探究单行多赋值计算
    _ = d                   //在本探究中没啥用的空行
}

对情况三来说,赋值的结果实际上是跟情况二是一样的:
1. 计算c、b、b+c的值(前两个无需计算),并存储在两个临时变量中(解释:为什么不是三个临时变量)
2. 将运算结果从临时变量中调出来,从左到右依次赋给b, c, d三个变量。

为什么不是三个临时变量?
对于前半部分 b, c = c, b ,想达成互换两个的值,只需要一个临时变量即可
temp := b
b = c
c = temp

  ; -----情况三  相关汇编代码  开始----------
  (.\main.go:6)     MOVQ    "".b+16(SP), AX           ; 将b的值,放入AX寄存器
  (.\main.go:6)     ADDQ    "".c+8(SP), AX            ; AX中的值加c的值(结果还保存在AX寄存器里)
  (.\main.go:6)     MOVQ    AX, ""..autotmp_6+40(SP)  ; 把上述计算结果赋值给autotmp_6变量
  (.\main.go:6)     MOVQ    "".b+16(SP), AX           ; 将b的值赋给AX
  (.\main.go:6)     MOVQ    AX, ""..autotmp_7+32(SP)  ; 把AX的值赋给autotmp_7变量
  (.\main.go:6)     MOVQ    "".c+8(SP), AX            ; 将c的值赋给AX
  (.\main.go:6)     MOVQ    AX, "".b+16(SP)           ; 把AX的值(c)赋给b
  (.\main.go:6)     MOVQ    ""..autotmp_7+32(SP), AX  ; 把autotmp_7变量的值赋给AX
  (.\main.go:6)     MOVQ    AX, "".c+8(SP)            ; 把AX的值(autotmp_7)赋给c
  (.\main.go:6)     MOVQ    ""..autotmp_6+40(SP), AX  ; 把autotmp_6变量的值赋给AX
  (.\main.go:6)     MOVQ    AX, "".d(SP)              ; 把AX的值(autotmp_6)赋给d
  ; -----情况三  相关汇编代码  结束----------

汇编代码详解:

实际应用(算法题)

《五行解决反转链表问题》


作者能力有限,若有哪些地方有误,请在评论区里指正。愿我们一同进步!

Go实习面试经(1)

概要

2020年11月5日,初次尝试面试,我对我自己个人品质方面还挺满意的,但是在回答问题、知识掌握程度等方面还有所欠缺。
经过本次面试后,我将自己的优点和不足,在本文章中讲进行总结,并对以后需要改进的地方进行规划。


对自己满意的地方

  • 远程面试的时候,我能感受到我的自信,说话简洁大方。
  • 远程面试钱,我对设备进行的充足的调试、在宿舍内划分出了独立的区域,避免了出现尴尬的情况。

自己的不足

  • 过于好胜,不轻易说自己不会。实际上并不掌握的东西,我也会蒙着回答。

    这并不是个很好的习惯,知之为知之,不知为不知。不清楚的地方可以回答不知道,一方面可以体现出自己实事求是的态度,二方面可以便于用人公司更好地评估自己的水平。

  • 被问到自己不会的题以后,会紧张,之后的问题回答的就有些不足。

    尽量直视自己不会的问题。多面试几次情况就能好一些。

  • 回答问题的总结能力不够。

    很多问题自己可以通过脑图、图像等形式解释清楚,但是到了网络面试的时候(以语音为主的情况下),自己的总结概况成文字描述能力不够,导致有时候我自己也不知道自己说道哪里了。(在讲解TCP握手的时候遇到了这个问题。)

  • 没有经历过在面试中编写算法代码的经历。

    本次面试,被问到了一道非常简单且常规的算法题(求最小公倍数),我虽然知道要怎么做,但是却不能说出自己具体的思路,感觉需要更多的时间去编写代码。

需要改进的地方TODO

  1. 优化自己对进程、线程、协程描述的语言。
  2. 继续练习算法,并且练熟算法。

面试总结

从题目来讲,面试的时候无论面试哪一个岗位,网络知识方面是必考的内容,需要重点掌握。其次面试了两家公司的go开发职位,都有问到进程、线程、协程的区别,对于goer来说,应该掌握这三者的区别。

面试问题:

1. 进程、线程、协程的区别

  • 进程(process):进程是指计算机中已运行的程序
  • 线程(thread):线程是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。
      同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
      一个进程可以有很多线程,每条线程并行执行不同的任务。
  • 协程(coroutine):协程是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。协程更适合于用来实现彼此熟悉的程序组件,如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道。

  协程非常类似于线程。但是协程是协作式多任务的,而线程典型是抢占式多任务的。协作式环境下,下一个进程被调度的前提是当前进程主动放弃时间片;抢占式环境下,操作系统完全决定进程调度方案,操作系统可以剥夺耗时长的进程的时间片,提供给其它进程。
  协程是语言层级的构造,可看作一种形式的控制流,而线程是系统层级的构造,可看作恰巧没有并行运行的线程。这两个概念谁有优先权是争议性的:线程可看作为协程的一种实现,也可看作实现协程的基底。

2. 网络协议相关问题

OCI是哪七层?

OSI七层网络模型 各层的解释 TCP/IP四层概念模型   对应网络协议
应用层(Application) 为应用程序提供服务 应用层 HTTP、TFTP, FTP, NFS, WAIS、SMTP
表示层(Presentation) 数据格式转换、数据加密 Telnet, Rlogin, SNMP, Gopher
会话层(Session) 建立、管理和维护会话 SMTP, DNS
传输层(Transport) 建立、管理和维护端到端的连接 传输层 TCP, UDP
网络层(Network) IP选址及路由选择 网络层 IP, ICMP, ARP, RARP, AKP, UUCP
数据链路层(Data Link) 提供介质访问和链路管理 数据链路层 FDDI, Ethernet, Arpanet, PDN, SLIP, PPP
物理层(Physical) 物理层 IEEE 802.1A, IEEE 802.2到IEEE 802.11

TCP、UDP属于哪一层?

见上表

TCP握手

符号 功能
SYN 请求建立连接符号位
FIN 请求关闭连接符号位
ACK 响应符号位
seq 序号(4字节)
ack
(ACKnum)
确认号(4字节)

建立连接时的握手:(三次握手)

为什么不能用两次或四次握手?

断开连接时的挥手:(四次挥手):

ping命令的细节(过程)

在浏览器中访问域名的过程

3. go协程相关问题

题目:运行一个goroutine协程,其内部是for死循环,在该协程之外,如何给定一个信号,使该协程结束?

func main() {
    ch := make(chan struct{})
    go func() {
        for true {
            if _, ok := <-ch; ok != false {
                runtime.Goexit()
            }
        }
    }
    //给一个信号
    ch <- struct{}{}
}

手写代码,没试过能不能用。

4. 算法相关问题

题目:编写一个求最小公倍数的函数。

Gin框架中使用数据库——Go Gin框架(七)

安装MySQL数据库

MySQL官方链接:https://dev.mysql.com/downloads/mysql/
Debian:sudo apt install mysql-5.7
安装过程:略

Go 安装MySQL驱动

go get "github.com/go-sql-driver/mysql"

在项目中使用MySQL

在go mod模式下,还需要在项目中引用mysql模块

go.mod文件

module MonaGinWeb

go 1.15

require (
    github.com/gin-gonic/gin v1.6.3
    github.com/go-sql-driver/mysql v1.5.0
)

在go中连接MySQL

import (
    "database/sql"
    "log"
    "strconv"
)

var db *sql.DB

func init() {
    c := MySQLConnInfo{
        user:       "",
        password:   "",
        connMethod: "tcp",
        host:       "",
        port:       0,
        dbname:     "",
        otherArgs:  "",
    }
    //数据库连接字符串
    //"user:password@tcp(host:port)/dbname"
    connStr := c.user + ":" + c.password +
        "@" + c.connMethod + "(" + c.host + ":" + strconv.Itoa(c.port) + ")/" +
        c.dbname + c.otherArgs
    if temp, err := sql.Open("mysql", connStr); err == nil {
        db = temp
    } else { //连接出现错误
        log.Fatal(err.Error())
        return
    }
}
  • func Open(driverName string, dataSourceName string) (*DB, error):该函数用于打开程序与SQL数据库的连接。其会返回创建(连接成功)的一个DB指针,有两个参数:
    • driverName string:用于指定数据库引擎
    • dataSourceName string:用于传入数据库的连接字符串

MySQL的增删改查

(待补充)

Gin网络请求与路由处理——Go Gin框架(二)

创建Engine(引擎)

Engine代表Gin框架的一个结构体定义。
其中包括了路由组、中间件、页面渲染接口、框架配置设置等相关内容

Engine 有以下两种创建方式:

engine1 = gin.Default() //通常使用,会默认使用Logger和Recovery中间件
engine2 = gin.New()

Recovery中间件的作用是:如果程序执行过程中遇到了panic中断了服务,Recovery会恢复程序的执行,并返回服务器500内部错误。
Logger负责打印并输出日志的中间件,方便我们开发调试。

实际上 gin.Default() 内部也是调用 gin.New() 来实现的,但是前者添加了上述两个中间件。

用Engine处理HTTP请求

在实例engine中,包含很多种方法可以直接处理不同类型的HTTP请求。

HTTP协议中义工定义了八种操作方式,分别是:OPTIONS、HEAD、GET、POST、PUT、DELETE、TRACE、CONNECT。
实际上开发上常用的也只有GET、POST、DELETE等几种操作方式。

通用处理请求

engine中可以直接进行HTTP请求的处理,在engine中,可以使用Handle方法进行HTTP请求的处理。Handle方法包含三个参数,如下所示:

func (group *RouterGroup) Handle(httpMethod, relativePath string, handles ...HandlerFunc) IRoutes
  • httpMethod string:第一个参数表示要处理的HTTP的请求类型,是"GET""POST""DELETE"等八种操作方式的其中一种
  • relativePath string:第二个参数表示要解析的接口,由开发者定义。
  • handlers Handlerfunc:第三个参数是处理对应的请求的代码的定义。

分类处理请求

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes
//等几个常用的请求处理方法

每个参数的具体含义,请参考通用处理请求中的参数说明。

实例

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

func main() {
    engine := gin.Default()
    //第一种处理http请求的方法,使用handle
    //访问 /hello?name=xxxxx
    engine.Handle("GET", "/hello", func(context *gin.Context) {
        //context里面封装了一个上下文环境变量,在这里面当中有我们经常操作的属性或者方法。
        path := context.FullPath() //获取我们本次请求的接口
        fmt.Println(path)
        //获取GET中的参数
        name := context.DefaultQuery("name", "null") //获取具体参数,1.哪个字段 2.若获取不到的默认值
        fmt.Println(name)
        //输出
        _, _ = context.Writer.Write([]byte("hello, " + name))
    })
    //第二种处理http请求的方法(分类处理)
    //访问 /login
    engine.POST("/login", func(context *gin.Context) {
        fmt.Println(context.FullPath())
        //解析POST
        username := context.PostForm("username")
        //这种方法会返回是否成功获取Form里的值,以进行判断
        password, 获取成功 := context.GetPostForm("password")
        if 获取成功 {
            fmt.Println(username, "n", password)
            _, _ = context.Writer.Write([]byte(username + "登录,密码为:" + password))
        }
    })

    //delete
    //在访问路径中,指定id的方法
    //即:/delete/user/123或/delete/user/456(最后一项是一个变量)
    engine.DELETE("/delete/user/:id", func(context *gin.Context) {
        //获取路径中的变量值
        userID := context.Param("id")
        fmt.Println("删除用户:" + userID)
        context.Writer.Write([]byte("删除用户:"+userID))
    })
    //运行这个引擎
    _ = engine.Run(":8088")
}

上述示例代码中,包含了几个新的函数:

  • func (c *Context) FullPath() string: 该函数返回被访问路由的全路径。对于没有找到的路径,返回空
  • func (c *Context) DefaultQuery(key string, defaultValue string) string: 该函数用于GET请求中,用于获取GET请求中的参数。
    • key string:要获取GET请求中的哪个字段
    • defaultValue string:若获取不到则返回的默认值
  • func (c *Context) PostForm(key string) string
  • func (c *Context) GetPostForm(key string) (string, bool): 这两个函数用于获取POST请求中的参数。
    • key string:要获取POST请求中的哪个字段
    • GETPostForm 的第二个bool返回值,用于返回是否成功获取到该字段
  • func (c *Context) Param(key string) string: 用于获取访问路径中的参数值。
    • key string:要获取访问路径请求中的哪个字段,该字段在路径中的表示方法为/:key
  • func (ResponseWriter) Write([]byte) (int, error):用于向浏览器端输出。(返回值占时不知道含义)