2012-06-10 5 views
8

Допустим, мы хотим реализовать следующие вычисления:Вложенные вызовы функций в GO

outval/err = f3(f3(f1(inval))

, где каждый из f1, f2, f3 может потерпеть неудачу с ошибкой в ​​то время мы прекращаем расчет и установить err к ошибке, возвращаемой функцией сбоя. (Конечно, раскрой может быть сколь угодно долго)

В таких языках, как C++/Java/C# это можно легко сделать, имея f1, f2 и f3 сгенерирует исключение и ограждающих вычисления в примерочных поймать блока, в то время как в такие языки, как Haskell, мы можем использовать вместо них монады.

Теперь я пытаюсь реализовать его в GO, и единственный подход, о котором я могу думать, - это очевидная лестница if-else, которая довольно многословна. У меня нет проблем, если мы не можем вложить вызовы, но, на мой взгляд, добавить проверку ошибок после того, как каждая строка в коде выглядит уродливой и она прерывает поток. Я хотел бы знать, есть ли лучший способ сделать это.

Edit: Редактирование в соответствии с комментарием по peterSO
Ниже приведен конкретный пример и прямой реализации

package main 

import "fmt" 

func f1(in int) (out int, err error) { 
    return in + 1, err 
} 

func f2(in int) (out int, err error) { 
    return in + 2, err 
} 

func f3(in int) (out int, err error) { 
    return in + 3, err 
} 

func calc(in int) (out int, err error) { 
    var temp1, temp2 int 
    temp1, err = f1(in) 
    if err != nil { 
     return temp1, err 
    } 
    temp2, err = f2(temp1) 
    if err != nil { 
     return temp2, err 
    } 
    return f3(temp2) 
} 

func main() { 
    inval := 0 
    outval, err := calc3(inval) 
    fmt.Println(inval, outval, err) 
} 

То, что я пытаюсь проиллюстрировать это, функция известково делает некоторые вычисления, возможно, с помощью функций библиотеки что может произойти сбой и семантика, если какой-либо вызов fail calc распространяет ошибку на вызывающего абонента (аналогично тому, как обрабатывать исключение). На мой взгляд, код для расчета является уродливым.

Между для данного конкретного случая, когда все функции библиотеки имеют точно такую ​​же подпись, мы можем сделать код лучше (я использую идею из http://golang.org/doc/articles/wiki/#tmp_269)

func saferun(f func (int) (int, error)) func (int, error) (int, error) { 
    return func (in int, err error) (int, error) { 
     if err != nil { 
      return in, err 
     } 
     return f(in) 
    } 
} 

Тогда мы можем переопределить известково, как

func calc(in int) (out int, err error) { 
    return saferun(f3)(saferun(f2)(f1(in))) 
} 

или

func calc(in int) (out int, err error) { 
    sf2 := saferun(f2) 
    sf3 := saferun(f3) 
    return sf3(sf2(f1(in))) 
} 

Но без дженериков с upport, я не уверен, как я могу использовать этот подход для любого набора функций библиотеки.

ответ

5

Обсуждение между ошибками и исключениями является длинным и утомительным. Поэтому я не буду вдаваться в это.

Самый простой ответ на ваш вопрос касается Go встроенный defer, panic и recover функций, как обсуждалось в this blog пост. Они могут предлагать поведение, подобное исключениям.

package main 

import "fmt" 

func main() { 
    defer func() { 
     // This recovers from a panic if one occurred. 
     if x := recover(); x != nil { 
      fmt.Printf("%v\n", x) 
     } 
    }() 

    value := f(f(f(1))) 
    fmt.Printf("%d\n", value) 
} 

func f(i int) int { 
    value := i*i + 1 

    // something goes wrong, panic instead of returning an error. 
    panic("ohnoes") 

    return value 
} 
+0

Спасибо, что ответили, но я не уверен, что понимаю ваш подход. Что делать, если я не контролирую подпись f1, f2, f3 и т. Д., Потому что они являются библиотечными функциями.В моем скромном мнении ошибки обработки инфраструктур на других языках не требуют, чтобы у вас был такой контроль – Suyog

+2

Suyog, цитируя ваш исходный вопрос: «... сделанный путем исключения f1, f2 и f3 исключения». похоже, что вы разрешаете изменять f1, f2 и f3 для исключения исключений. Их паника ничем не отличается. Паника - это механизм, доступный в Go, чтобы разматывать стек с произвольной глубины, возвращая значение в процессе. Это не идиоматический и предпочтительный способ обработки ошибок в Go, но именно механизм будет делать то, что вы просите. – Sonia

+0

Я имел в виду, что f1, f2, f3 уже определены для исключения. Извините за плохую формулировку. На языке, подобном JAVA, многие функции библиотеки определены для исключения исключений, тогда как в GO это выглядит так, чтобы функции библиотеки возвращали ошибку. Поэтому проблема, с которой я столкнулась, - это проверить ошибки сразу, что приводит к написанию повторяющегося кода. – Suyog

0

Без конкретного примера вы наклоняетесь на ветряные мельницы. Например, для вашего определения функции fn возвращают значение и любую ошибку. Функции fn - это функции пакета, подпись которых не может быть изменена. Используя ваш пример,

package main 

import "fmt" 

func f1(in int) (out int, err error) { 
    return in + 1, err 
} 

func f2(in int) (out int, err error) { 
    return in + 2, err 
} 

func f3(in int) (out int, err error) { 
    return in + 3, err 
} 

func main() { 
    inval := 0 
    outval, err := f3(f2(f1(inval))) 
    fmt.Println(inval, outval, err) 
} 

Как вы собираетесь использовать свой пример для компиляции и запуска?

+0

Спасибо за ответ, я обновляю вопрос в соответствии с предложениями – Suyog

7

Если вы действительно хотите, чтобы это сделать, вы можете использовать функцию компоновки.

func compose(fs ...func(Value) (OutVal, error)) func(Value) (OutVal, error) { 
    return func(val Value) OutVal, Error { 
    sVal := val 
    var err error 
    for _, f := range fs { 
     sval, err = f(val) 
     if err != nil { 
     // bail here and return the val 
     return nil, err 
     } 
    } 
    return sval, nil 
    } 
} 

outVal, err := compose(f1, f2)(inVal) 

Большую часть времени, хотя вы, вероятно, хотите быть более простым, чем это, так как это может быть трудно для других, чтобы понять ваш код, когда они сталкиваются с ней.

+0

Это было полезно, спасибо! – Suyog

7

Во-первых, расширенная версия стиля try-catch, к которой вы привыкли, заимствуя, очевидно, ответ jimt и ответ PeterSO.

package main 

import "fmt" 

// Some dummy library functions with different signatures. 
// Per idiomatic Go, they return error values if they have a problem. 
func f1(in string) (out int, err error) { 
    return len(in), err 
} 

func f2(in int) (out int, err error) { 
    return in + 1, err 
} 

func f3(in int) (out float64, err error) { 
    return float64(in) + .5, err 
} 

func main() { 
    inval := "one" 

    // calc3 three is the function you want to call that does a computation 
    // involving f1, f2, and f3 and returns any error that crops up. 
    outval, err := calc3(inval) 

    fmt.Println("inval: ", inval) 
    fmt.Println("outval:", outval) 
    fmt.Println("err: ", err) 
} 

func calc3(in string) (out float64, err error) { 
    // Ignore the following big comment and the deferred function for a moment, 
    // skip to the comment on the return statement, the last line of calc3... 
    defer func() { 
     // After viewing what the fXp function do, this function can make 
     // sense. As a deferred function it runs whenever calc3 returns-- 
     // whether a panic has happened or not. 
     // 
     // It first calls recover. If no panic has happened, recover returns 
     // nil and calc3 is allowed to return normally. 
     // 
     // Otherwise it does a type assertion (the value.(type) syntax) 
     // to make sure that x is of type error and to get the actual error 
     // value. 
     // 
     // It does a tricky thing then. The deferred function, being a 
     // function literal, is a closure. Specifically, it has access to 
     // calc3's return value "err" and can force calc3 to return an error. 
     // A line simply saying "err = xErr" would be enough, but we can 
     // do better by annotating the error (something specific from f1, 
     // f2, or f3) with the context in which it occurred (calc3). 
     // It allows calc3 to return then, with this descriptive error. 
     // 
     // If x is somehow non-nil and yet not an error value that we are 
     // expecting, we re-panic with this value, effectively passing it on 
     // to allow a higer level function to catch it. 
     if x := recover(); x != nil { 
      if xErr, ok := x.(error); ok { 
       err = fmt.Errorf("calc3 error: %v", xErr) 
       return 
      } 
      panic(x) 
     } 
    }() 
    // ... this is the way you want to write your code, without "breaking 
    // the flow." 
    return f3p(f2p(f1p(in))), nil 
} 

// So, notice that we wrote the computation in calc3 not with the original 
// fX functions, but with fXp functions. These are wrappers that catch 
// any error and panic, removing the error from the function signature. 
// Yes, you must write a wrapper for each library function you want to call. 
// It's pretty easy though: 
func f1p(in string) int { 
    v, err := f1(in) 
    if err != nil { 
     panic(err) 
    } 
    return v 
} 

func f2p(in int) int { 
    v, err := f2(in) 
    if err != nil { 
     panic(err) 
    } 
    return v 
} 

func f3p(in int) float64 { 
    v, err := f3(in) 
    if err != nil { 
     panic(err) 
    } 
    return v 
} 
// Now that you've seen the wrappers that panic rather than returning errors, 
// go back and look at the big comment in the deferred function in calc3. 

Итак, вы можете протестовать против того, что вы попросили об этом проще, и это не так. Нет аргументов в целом, но если библиотека выполняет все возвращаемые значения ошибок, и вы хотите связать вызовы функций без значений ошибок, доступно решение для обертывания библиотечных функций, а обертки очень тонкие и легкие для записи. Единственная другая сложная часть - это отложенная функция, но это шаблон, который вы можете изучить и повторно использовать, и это всего лишь несколько строк кода.

Я не хочу рекламировать это решение слишком много, потому что это не тот, который используется часто. Это действительный шаблон, хотя и имеет некоторые варианты использования, когда это уместно.

Обработка ошибок - большая тема, как упоминалось в jimt. «Каковы хорошие способы обработки ошибок в Go?» был бы хорошим вопросом для SO, за исключением проблемы, что он терпит неудачу в «цельной книге». I может представить целую книгу на тему обработки ошибок в Go.

Вместо этого я представлю себе общее замечание, что если вы только начнете работать с ошибками, а не пытаетесь заставить их исчезнуть, через некоторое время вы начнете понимать преимущества этого. То, что выглядит как многословная лестница, если утверждения в примере с игрушкой, как мы использовали здесь, может по-прежнему выглядеть сложной лестницей операторов if, когда вы впервые пишете ее в реальной программе мира. Когда вам действительно нужно обрабатывать эти ошибки, вы возвращаетесь к коду и внезапно видите его в виде заглушек, ожидающих, пока вы будете работать с реальным кодом обработки ошибок. Вы можете видеть, что делать, потому что код, вызвавший ошибку, находится прямо там. Вы можете заставить пользователя увидеть сообщение об ошибке с низким уровнем ошибок и вместо этого показать что-то значимое. Вам, как программисту, предлагается сделать правильную вещь вместо того, чтобы принимать вещь по умолчанию.

Для получения более полных ответов один хороший ресурс для начала - статья Error Handling and Go. Если вы выполните поиск по Go-Nuts messages, там будут длительные дискуссии по этому вопросу. Функции в стандартной библиотеке вызывают друг друга совсем немного (сюрприз), поэтому исходный код стандартной библиотеки содержит много примеров обработки ошибок. Это отличные примеры для изучения, поскольку код написан авторами Go, которые продвигают этот стиль программирования при работе со значениями ошибок.

+0

Большое спасибо за обширный ответ! – Suyog

0

Найдено по почте thread на гайках по этой теме. Добавьте его для справки.

0

Слишком плохо это один уже закрыт ... Это:

value := f(f(f(1))) 

не пример цепочки, но вложенности. Цепной должен выглядеть примерно так:

c.funA().funB().funC() 

Вот это работает example.

+0

Справедливая точка; это не ответ, а комментарий, хотя – leonbloy

Смежные вопросы