英文:
Can I create a function that must only be used with defer?
问题
例如:
package package
// 亲爱的用户,CleanUp 只能与 defer 一起使用:defer CleanUp()
func CleanUp() {
// 检查调用是否被延迟的一些逻辑
// 执行清理操作
}
在用户代码中:
func main() {
package.CleanUp() // 报错,CleanUp 必须被延迟执行!
}
但是如果用户这样运行代码:
func main() {
defer package.CleanUp() // 做得好,没有报错
}
我已经尝试过的方法:
func DeferCleanUp() {
defer func() { /* 执行清理操作 */ }()
// 但后来我意识到这正好与我所需相反
// 用户不再需要调用 defer CleanUp,但是...
}
// 现在如果 API 被错误使用,也会导致问题:
defer DeferCleanUp() // 一个延迟的嵌套,问题仍然存在。
英文:
For example:
package package
// Dear user, CleanUp must only be used with defer: defer CleanUp()
func CleanUp() {
// some logic to check if call was deferred
// do tear down
}
And in userland code:
func main() {
package.CleanUp() // PANIC, CleanUp must be deferred!
}
But all should be fine if user runs:
func main() {
defer package.CleanUp() // good job, no panic
}
Things I already tried:
func DeferCleanUp() {
defer func() { /* do tear down */ }()
// But then I realized this was exactly the opposite of what I needed
// user doesn't need to call defer CleanUp anymore but...
}
// now if the APi is misused it can cause problems too:
defer DeferCleanUp() // a defer inception xD, question remains.
答案1
得分: 4
好的,以下是翻译好的内容:
好的,根据 OP 的要求,只是为了好笑,我将发布这种通过查看调用堆栈并应用一些启发式方法来解决的笨拙方法。
免责声明:不要在真实代码中使用这个方法。我认为检查延迟执行甚至都不是一件好事。
还要注意:这种方法只在可执行文件和源代码位于同一台机器上时才有效。
链接到 gist: https://gist.github.com/dvirsky/dfdfd4066c70e8391dc5(在 playground 中无法工作,因为无法在那里读取源文件)
package main
import (
"fmt"
"runtime"
"io/ioutil"
"bytes"
"strings"
)
func isDeferred() bool {
// 首先获取调用者的名称
var caller string
if fn, _, _, ok := runtime.Caller(1); ok {
caller = function(fn)
} else {
panic("No caller")
}
// 让我们查看比这个函数高 2 级的调用者 - 第一级是这个函数本身,
// 第二级是 CleanUp()
// 我们想要的是调用 CleanUp() 的那个
if _, file, line, ok := runtime.Caller(2); ok {
// 现在我们实际上需要读取源文件
// 当然,这应该被缓存以避免糟糕的性能
// 我从 runtime/debug 中复制了这个,所以这是合法的 :)
data, err := ioutil.ReadFile(file)
if err != nil {
panic("Could not read file")
}
// 现在让我们读取调用者所在的确切行
lines := bytes.Split(data, []byte{'\n'})
lineText := strings.TrimSpace(string(lines[line-1]))
fmt.Printf("Line text: '%s'\n", lineText)
// 现在让我们应用一些丑陋的经验法则。这是脆弱的部分
// 可以通过正则表达式或实际的 AST 解析来改进,但是...
return lineText == "}" || // 在简单的延迟执行中,我们得到的就是这个
!strings.Contains(lineText, caller) || // 这处理了 defer func() { CleanUp() }() 的情况
strings.Contains(lineText, "defer ")
} // not ok - 意味着我们至少没有从 3 层深处调用
return false
}
func CleanUp() {
if !isDeferred() {
panic("Not Deferred!")
}
}
// 这不应该引发 panic
func fine() {
defer CleanUp()
fmt.Println("Fine!")
}
// 这也不应该引发 panic
func alsoFine() {
defer func() { CleanUp() }()
fmt.Println("Also Fine!")
}
// 这应该引发 panic
func notFine() {
CleanUp()
fmt.Println("Not Fine!")
}
// 从标准库的 runtime/debug 中获取:
// 如果可能,函数返回包含 PC 的函数的名称。
func function(pc uintptr) string {
fn := runtime.FuncForPC(pc)
if fn == nil {
return ""
}
name := fn.Name()
if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 {
name = name[lastslash+1:]
}
if period := strings.Index(name, "."); period >= 0 {
name = name[period+1:]
}
name = strings.Replace(name, "·", ".", -1)
return name
}
func main(){
fine()
alsoFine()
notFine()
}
英文:
Alright, per OPs request and just for laughs, I'm posting this hacky approach to solving this by looking at the call stack and applying some heuristics.
DISCLAIMER: Do not use this in real code. I don't think checking deferred is even a good thing.
Also Note: this approach will only work if the executable and the source are on the same machine.
Link to gist: https://gist.github.com/dvirsky/dfdfd4066c70e8391dc5 (this doesn't work in the playground because you can't read the source file there)
package main
import(
"fmt"
"runtime"
"io/ioutil"
"bytes"
"strings"
)
func isDeferred() bool {
// Let's get the caller's name first
var caller string
if fn, _, _, ok := runtime.Caller(1); ok {
caller = function(fn)
} else {
panic("No caller")
}
// Let's peek 2 levels above this - the first level is this function,
// The second is CleanUp()
// The one we want is who called CleanUp()
if _, file, line, ok := runtime.Caller(2); ok {
// now we actually need to read the source file
// This should be cached of course to avoid terrible performance
// I copied this from runtime/debug, so it's a legitimate thing to do :)
data, err := ioutil.ReadFile(file)
if err != nil {
panic("Could not read file")
}
// now let's read the exact line of the caller
lines := bytes.Split(data, []byte{'\n'})
lineText := strings.TrimSpace(string(lines[line-1]))
fmt.Printf("Line text: '%s'\n", lineText)
// Now let's apply some ugly rules of thumb. This is the fragile part
// It can be improved with regex or actual AST parsing, but dude...
return lineText == "}" || // on simple defer this is what we get
!strings.Contains(lineText, caller) || // this handles the case of defer func() { CleanUp() }()
strings.Contains(lineText, "defer ")
} // not ok - means we were not clled from at least 3 levels deep
return false
}
func CleanUp() {
if !isDeferred() {
panic("Not Deferred!")
}
}
// This should not panic
func fine() {
defer CleanUp()
fmt.Println("Fine!")
}
// this should not panic as well
func alsoFine() {
defer func() { CleanUp() }()
fmt.Println("Also Fine!")
}
// this should panic
func notFine() {
CleanUp()
fmt.Println("Not Fine!")
}
// Taken from the std lib's runtime/debug:
// function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) string {
fn := runtime.FuncForPC(pc)
if fn == nil {
return ""
}
name := fn.Name()
if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 {
name = name[lastslash+1:]
}
if period := strings.Index(name, "."); period >= 0 {
name = name[period+1:]
}
name = strings.Replace(name, "·", ".", -1)
return name
}
func main(){
fine()
alsoFine()
notFine()
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论