实验楼-楼赛 第15期 Go语言-解题报告

前记:本解题方法仅为一家之言


有任何不明题意的地方,请自行移步:https://www.shiyanlou.com/contests/lou15/challenges


第一题:透明地修改HTTP请求


题意说明:

      算出http请求body的md5值,加到header里(X-Md5),如果没有body,则不加X-Md5。


解题思路:

      这道题很简单了,实现http.RoundTripper接口。读出http.body,计算md5,放入header。


解题代码:



注意事项:

      1. req.Body有可能为nil

      2. req.body==nil || req.body.len==0的情况下不设置header.X-Md5;

      3. 记得req.Body.Close(),见$GOROOT/src/net/http/request.go: line 164;即便抛开这个理由,你把人body都狸猫换太子了,总要记得close body吧。



一家之言:

      1. 不管有没有body,统一处理,不搞特殊化;

      2. 组合操作,不明显地操作数据;


后来想想,多数时候代码可能会写成下面这样:


先把数据读出,再做具体操作。这样的好处很明显,大家都明白在干什么,上面解法却不能一眼看懂。不过我还是用上面解法过了,逼格高嘛,呵呵。流操作,免去了自己操作变量的负担。


第二题:缓存请求执行结果


题意说明:

      说白了,就是做个本地缓存,过期更新。


解题思路:

      记录每个key对应的val和expire,超时则调用fn()重新获取。


解题代码:

package cacheflight

import (
	"hash/crc32"
	"sync"
	"time"
)

const bSize = 131

type value struct {
	time time.Time
	val  interface{}
	err  error
	mut  sync.Mutex
}

// Group is core struct
type Group struct {
	buckets []map[string]*value
	muts    []*sync.Mutex
	expire  time.Duration
}

// NewGroup return a new ttl cache group
func NewGroup(cacheExpiration time.Duration) (group *Group) {
	group = &Group{
		buckets: make([]map[string]*value, bSize),
		muts:    make([]*sync.Mutex, bSize),
		expire:  cacheExpiration,
	}
	for i := range group.buckets {
		group.buckets[i] = make(map[string]*value)
		group.muts[i] = &sync.Mutex{}
	}
	return
}

// Do cache
func (g *Group) Do(key string, fn func() (interface{}, error)) (ret interface{}, err error) {
	idx := crc32.ChecksumIEEE([]byte(key)) % uint32(len(g.buckets))
	g.muts[idx].Lock()
	val := g.buckets[idx][key]
	if val == nil {
		val = &value{}
		g.buckets[idx][key] = val
	}
	g.muts[idx].Unlock()

	val.mut.Lock()
	if val.err == nil && time.Since(val.time) < g.expire {
		ret = val.val
		err = val.err
	} else {
		ret, err = fn()
		val.val, val.err = ret, err
		val.time = time.Now()
	}
	val.mut.Unlock()
	return
}

注意事项:

      测试要求有且只有一个协程调用fn(),所以一定要控制好并发。


一家之言:

      其实我伊始不是用上面方法解题的,可以看出对同一个key完全串行化,简单粗暴,但同时失去了同一个key并发的机会。嗯,装逼的我一开始就去掉相同key的这把锁,结果一直失败,把我惹毛了,直接简单粗暴来了上面一版解题成功。经我不懈努力,还是做到了如下代码:

package cacheflight

import (
	"hash/crc32"
	"sync"
	"sync/atomic"
	"time"
)

const bSize = 131

type value struct {
	time time.Time
	val  interface{}
	err  error
	wait chan struct{}
	mut  sync.RWMutex
	cnt  uint32
}

// Group is core struct
type Group struct {
	buckets []map[string]*value
	muts    []*sync.Mutex
	expire  time.Duration
}

// NewGroup return a new ttl cache group
func NewGroup(cacheExpiration time.Duration) (group *Group) {
	group = &Group{
		buckets: make([]map[string]*value, bSize),
		muts:    make([]*sync.Mutex, bSize),
		expire:  cacheExpiration,
	}
	for i := range group.buckets {
		group.buckets[i] = make(map[string]*value)
		group.muts[i] = &sync.Mutex{}
	}
	return
}

// Do cache
func (g *Group) Do(key string, fn func() (interface{}, error)) (ret interface{}, err error) {
	if g.expire <= 0 {
		return fn()
	}

	idx := crc32.ChecksumIEEE([]byte(key)) % uint32(len(g.buckets))
	g.muts[idx].Lock()
	val := g.buckets[idx][key]
	if val == nil {
		val = &value{wait: make(chan struct{})}
		g.buckets[idx][key] = val
	}
	g.muts[idx].Unlock()

	val.mut.RLock()
	if val.err == nil && time.Since(val.time) < g.expire {
		ret = val.val
		err = val.err
		val.mut.RUnlock()
		return
	}
	wait := val.wait
	val.mut.RUnlock()

	if atomic.AddUint32(&val.cnt, 1) == 1 {
		ret, err = fn()

		val.mut.Lock()
		val.val, val.err = ret, err
		val.time = time.Now()
		close(val.wait)
		val.wait = make(chan struct{})
		val.mut.Unlock()

		atomic.StoreUint32(&val.cnt, 0)
		return
	}

	<-wait
	return val.val, val.err
}
没错,用读锁让同一个key的也并发起来,用原子操作决定只有第一个调用fn(),后面进来的协程统一wait。实际应用中,fn()有io操作肯定是要慢一些的,所以不把它放在锁里面。当返回后,加锁写val,通知其他协程可读了,同时准备好下次的wait信号。写完后记得重置记数。其他协程经过wait后读数据不加锁,不加锁,不加锁,重要的事情说三遍,因为此时刚写完,要经一段时间后才会有写,所以此时的读不需要加任何锁,放心大胆地读就是了。

理论上,inc(val.cnt)reset(val.cnt, 0)并发会有问题,一是经写锁拦住了漏下来的协程第一句是inc(),二是经过调用加写锁、写数据这些步骤后,还有协程没有做inc()操作的机率能有多大?所以这种情况理论上会出现,但实际中出现概率低到几乎不可能,可忽略掉。

还有一个问题是atomic.inc真的会比lock;n++;unlock这种模式快吗?在我测试结果看来是这样的,至少3倍以上,多数时候是4倍左右。

第三题:实现json注释解析器


题意说明:

      在json中'#'之后的都是注释。


解题思路:

      扫一遍字符串,找到'#'返回即可。


解题代码:



注意事项:

      1. 在字符串中,字符'"'是用'\"'的形式表示的,所以会有反斜线'\\'转义。


一家之言:

      注意记录状态,是否在字符串内,如果在字符串内有#是要略过的,其他时候完全忽略错误是因为题目给的这个函数完全没有err返回值。在判断'\\'时应该判断是不是在字符串内。

      第一次看到题目时,以为要解析像/*comment*/这种注释呢,心中有些期待,结果看到题目后有些小失望呢。


最后再吹两句:

整体来说,三道题都有一定的实用价值,可以看出出题者用心了。

第一题是语言相关的,很常见,对接口的理解有一定要求;

第二题可以说有点语言影子,但其实这类问题是不区分语言一定要解决的了;

第三题完全可以说语言无关了,任何一门语言都可以解决。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值