前记:本解题方法仅为一家之言
有任何不明题意的地方,请自行移步: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*/这种注释呢,心中有些期待,结果看到题目后有些小失望呢。
最后再吹两句:
整体来说,三道题都有一定的实用价值,可以看出出题者用心了。
第一题是语言相关的,很常见,对接口的理解有一定要求;
第二题可以说有点语言影子,但其实这类问题是不区分语言一定要解决的了;
第三题完全可以说语言无关了,任何一门语言都可以解决。