I use something like that, it redo the transaction as a call and that way you get the failing message.
func GetFailingMessage(client ethclient.Client, hash common.Hash) (string, error) {
tx, _, err := client.TransactionByHash(context.Background(), hash)
if err != nil {
return "", err
}
from, err := types.Sender(types.NewEIP155Signer(tx.ChainId()), tx)
if err != nil {
return "", err
}
msg := ethereum.CallMsg{
From: from,
To: tx.To(),
Gas: tx.Gas(),
GasPrice: tx.GasPrice(),
Value: tx.Value(),
Data: tx.Data(),
}
res, err := client.CallContract(context.Background(), msg, nil)
if err != nil {
return "", err
}
return string(res), nil
}