距离上次一次blog更新时隔将近两年了。20年为了毕业,搞了半年的nlp的实践(知识图谱-KnowledgeGraphInference)。下半年刚开始打工,这将近一年的时间里做了很多关于业务工作,但无法公开分享,主要是职场新人,这里面水太深,把握不住(其实是懒)。现在重新回归,继续更新我的blog。

目标

解答日常在代码审计中常见模糊点。

已有基础

  1. 简单了解golang语法,0 golang编写基础
  2. 能够熟练复制别人的代码并跑过编译
  3. 熟练掌握面向对象编程
  4. 大概知道go的struct是个啥。通过下面这篇文章。

《Go Struct超详细讲解》
https://juejin.cn/post/6844903814168838151

了解了以下几个点:

struct 定义如下

1
2
3
4
5
6
7
8
9
type Member struct {
key1 int
key2, key3 string
}

var (
m1 = Member{id:1}
)
var m1 Member
  1. 结构体也可以不包含任何字段,称为空结构体,struct{}表示一个空的结构体,注意,直接定义一个空的结构体并没有意义,但在并发编程中,channel之间的通讯,可以使用一个struct{}作为信号量。
1
2
ch := make(chan struct{})
ch <- struct{}{}
  1. 结构体中未定义的值默认为空

如果是int 则为0 ,string 则为 “”, 不是单引

  1. 未初始化的结构体指针为nil

  2. new函数初始化结构体,返回指针

  3. 首字母小写代表包外不可见

  4. 直接给类型代表该字段是匿名字段,可以实现继承的操作。

net/http源码

以常见的使用入口为切入点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"log"
"net/http"
"time"
)

type myHandler struct {
output string
}

func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_,err := w.Write([]byte(h.output))
if err != nil {
fmt.Println(err)
}
}

func main() {
s := &http.Server{
Addr: ":8080",
Handler: &myHandler{output: "Hello world!"},
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
}

http.server

Server 结构体

https://golang.org/pkg/net/http/#Server

1
2
3
4
5
6
7
8
// Addr optionally specifies the TCP address for the server to listen on,
// in the form "host:port". If empty, ":http" (port 80) is used.
// The service names are defined in RFC 6335 and assigned by IANA.
// See net.Dial for details of the address format.
// Addr 不深入了
Addr string

Handler Handler // handler to invoke, http.DefaultServeMux if nil

type Handler

https://golang.org/pkg/net/http/#Handler

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

Handler是一个interface

So what’s Interface in golang?

Google了一下,随便找了篇文章读了一下。

14 Go语言–接口interface详解

复制一下能理解的几个点,没复制的都是看不懂的:

  • 接口是一个或多个方法签名的集合
  • 只要某个类型拥有该接口的所有方法签名,即算实现该接口,无需显示声明实现了哪个接口,这称为 Structural Typing

  • 接口只有方法声明,没有实现,没有数据字段
  • 接口可以匿名嵌入其它接口,或嵌入到结构中
  • 将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针
  • 只有当接口存储的类型和对象都为nil时,接口才等于nil
  • 空接口可以作为任何类型数据的容器

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
)

type A interface {
test(int)
}

type T struct {
a int
// 显性实现
A
}

func (t T)test(i int){
fmt.Println("i:",i,", a:",t.a)
}

func main() {
var t = T{a:123}
t.test(1234)
}

也可以进行隐形实现,即Structural Typing

看完后的疑问

1. 接口定义的方法和结构体方法的区别

  1. 接口实现的方法应该可以是无receiver的,但这样意义不大,目前没想到又哪些场景需要通过接口来实现这样的方法。
  2. 主要是为了多态的考虑吧,接口定义的方法可以被多个类型进行实现

2. 在类型中实现了接口的情况下,对象的类型是type还是interface?

经过测试,通过reflect.TypeOf显示的类型是type,但通过

1
2
3
4
_,ok := i.(Interface)
if ok{
fmt.Println("Interface type")
}

也是可以的,所以interface应该是type的超集。

这里的应用场景鄙人理解有以下作用,当存在多个类型实现了接口的情况下,方法接受的类型无法确定的时候,使用Interface类型,再通过i.(type)进行switch判断,对不同的type进行不同的操作。

这里从文章中写的接口内部实现也可以得到一定的应证。

从接口的实现中,可以看出,无类型的interface可以接受任意类型,因为type *type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 没有方法的interface
type eface struct {
_type *_type //类型信息
data unsafe.Pointer //数据指针
}

// 记录着Go语言中某个数据类型的基本特征,_type是go所有类型的公共描述
//可以简单的认为,接口可以通过一个 _type *_type 直接或间接表述go所有的类型就可以了
type _type struct {
size uintptr //类型的大小
ptrdata uintptr //存储所有指针的内存前缀的大小
hash uint32 //类型的hash
tflag tflag //类型的tags
align uint8 //结构体内对齐
fieldalign uint8 //结构体作为field时的对齐
kind uint8 //类型编号 定义于 runtime/typekind.go
alg *typeAlg // 类型元方法 存储hash 和equal两个操作。
gcdata *byte //GC 相关信息
str nameOff //类型名字的偏移
ptrToThis typeOff
}

在有方法的接口中,由于可能多个type实现同一个interface(多态),接口实际在itab结构体中指向当时真正实现的类型,所以receiver可以通过dot来进行调用,因为隐形实现了这个接口,相同的函数名和参数类型(签名)。

回到Handler

这里的Handler为一个接口类型。

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

故存在不同的类型对ServeHTTP进行实现,这里传入的使用用户自定义的ServeHTTP方法,故必然存在类似的转换。可以看到实现了ServeHTTP有ServeMux和HandlerFunc进行是了实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// HandlerFunc
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

// ServeMux
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}

在自定义函数的调用下Handler ≠ nil 所以应该是HandlerFunc中进行了相应的类型转换,先按下不表,跟进ListenAndServe

func (srv *Server) ListenAndServe()

var xxxxx func(*paramDefine)

见到一个没有见过的东西,

1
2
3
4
5
6
var testHookServerServe func(*Server, net.Listener)

func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}

搜了一下,这里定义了一个方法签名(规定了参数类型),符合这个签名的func都可以赋值给这个xxxxx,在实际调用中,判断fn:=testHookServerServe,如果fn不为空说明有相应的函数实现了这个签名,再通过fn(srv, l)进行调用。

在export_test.go文件中,SetTestHookServerServe接受这个方法签名的参数,赋值给testHookServerServe变量。

1
func SetTestHookServerServe(fn func(*Server, net.Listener)) { testHookServerServe = fn }

其中一处实现为

1
2
3
SetTestHookServerServe(func(s *Server, ln net.Listener) {
serving <- true
})

当此处有定义时,会先执行这个func(s *Server, ln net.Listener)内的逻辑,golang中确实比较灵活,感觉似曾相识,一时又想不起来,可能也是多态的一种表现形式吧。

在此见到一个新东西 ←

Channel <-

Go Channel 详解

1
2
3
chan T          // 可以接收和发送类型为 T 的数据
chan<- float64 // 只可以用来发送 float64 类型的数据
<-chan int // 只可以用来接收 int 类型的数据

如果没有设置容量,或者容量设置为0, 说明Channel没有缓存,只有sender和receiver都准备好了后它们的通讯(communication)才会发生(Blocking)。如果设置了缓存,就有可能不发生阻塞, 只有buffer满了后 send才会阻塞, 而只有缓存空了后receive才会阻塞。一个nil channel不会通信。

像是一个broker?有缓存的长度的话是异步,否则是同步的,没有什么特殊的点。golang中还有一个关键字 defer,通过该关键字对资源进行释放,在return前执行。golang的return有一个特点,比如return value ,是先赋值,在return,而defer是插在两者中间的执行动作。

go 关键字

go 关键字用来创建 goroutine (协程),实现并发。

回到func (srv *Server) ListenAndServe()

在这个ServeHTTP的逻辑中,可以很清晰的看出,判断用户传入的handler是否为空,如果是空的话handler就为DefaultServeMux,然后调用其中ServeHTTP方法,在我们文章开头的例子中,我们的handler为myHandler,故会调用到相应的方法中。

具体的路由可以通过判断r.URL.Path来进行自定义,截至此处可以看到net/http提供了强大的基础能力,相应的框架实现都可以通过自己实现相应的方法进行封装。

水文,溜了~

⬆︎TOP